de.comp.lang.php.* FAQ

alles in einer einzigen Datei

     Build Date: Mon Jun 11 05:33:02 CEST 2007

  1. Über diese FAQ
    1. Was ist das hier?
    2. Wie ist die Charta dieser Newsgroup?
    3. Wo finde ich die aktuelle Version dieser FAQ?
    4. Kann ich eine Kopie der FAQ per Mail zugesendet bekommen?
    5. Du hast doch für die FAQ geschrieben. Ich habe da eine Frage zu PHP...
    6. Darf ich die FAQ auf meiner Site einbinden? Darf ich sie für meine Zwecke verwenden?
    7. Was soll ich tun, wenn ich einen Fehler in der FAQ gefunden habe?
    8. Kann ich selber für diese FAQ schreiben?
    9. Soll ich Jobangebote in de.comp.lang.php.misc posten?
    10. Wer kann mir einen Provider empfehlen?
    11. Warum bekomme ich Ermahnungsmails, wenn ich Autoren in der Gruppe auf Netiquetteverstöße aufmerksam mache?
    12. Warum bekomme ich Ermahnungsmails?
    13. Warum sind Flames sinnlos?
    14. Ich verwende Outlook Express und keiner hat mich lieb.
    15. Was ist TOFU? Wieso finden die Anderen meine Artikel schwer zu lesen?
    16. Wie verweise ich auf die FAQ?
    17. Wie stelle ich meine Frage an die Newsgroup am sinnvollsten?
    18. Warum kann ich nicht mehr auf de.comp.lang.php zugreifen?
  2. Allgemeine Fragen rund um PHP
    1. Was ist PHP?
    2. Welche Version von PHP ist aktuell?
    3. Wo bekomme ich PHP?
    4. Was ist neu in PHP5.1?
    5. Was ist neu in PHP5?
    6. Was ist neu in PHP4?
    7. Wo finde ich weitere Informationen über PHP?
    8. Welche Editoren sind für PHP geeignet?
    9. Gibt es für PHP einen "Dokumentationsgenerator" ähnlich Javadoc für Java?
    10. Was bedeutet LAMP, WAMP und so weiter?
    11. Gibt es Application Server für PHP?
    12. Was passiert, wenn ich eine PHP-Seite aufrufe?
    13. Wer ist der komische Typ mit den Bleistiften im Mund?
  3. Installation und Inbetriebnahme
    1. Wie kompiliere ich ein aktuelles PHP auf Linux mit Apache Server?
    2. Ich habe Probleme PHP selbst zu kompilieren.
    3. Wie installiere ich CGI-PHP auf einem Apache-Server?
    4. Wie installiere ich PHP auf Windows?
    5. Linux: Meine shared libraries werden nicht gefunden.
    6. SuSE Linux: Beim compilieren wird lex nicht gefunden
  4. Konfiguration
    1. Wie finde ich heraus, wie mein PHP-Interpreter konfiguriert ist?
    2. Wo finde ich die php.ini?
    3. Was bedeuten master value und local value in phpinfo()?
    4. Welche Konfigurationsvariablen kann ich nicht in .htaccess-Dateien verwenden?
    5. Was ist --enable-force-cgi-redirect? Warum enthält $_SERVER['PHP_SELF'] den Pfad zum CGI-Interpreter?
    6. Warum funktioniert set_time_limit() nicht wie angepriesen?
    7. Apache: Kann ich PHP auch auf .html-Dateien anwenden?
    8. Wie kann ich PHP (CGI und Apache-Modul) konfigurieren?
    9. Ich habe eine Frage zur Webserver-Konfiguration
    10. Meine Änderungen mit ini_set() haben keine Wirkung. Wie kann ich Konfigurationsvariablen zur Laufzeit ändern?
  5. Fragen zum PHP Interpreter
    1. Wie vergleicht sich PHP mit anderen bekannten Webentwicklungssystemen?
    2. Wie vergleicht sich die Performance von PHP zu Perl?
    3. Wie kann ich mein ASP-Programm in PHP übersetzen?
    4. CGI PHP oder Modul?
    5. Portable PHP-Scripte
    6. Zeitgesteuerte PHP-Scripte und Shellscripte
    7. Wie bette ich PHP in HTML ein? (Beispielprogramm)
    8. Wie kann ich auf Umgebungsvariablen zugreifen?
    9. Wie kann ich auf den HTTP-Request-Header zugreifen?
    10. Gibt es noch mehr interessante Variablen im Environment?
    11. Wie kann ich auf Kommandozeilen-Argumente zugreifen?
    12. Wie kann ich einen Parameter von einer PHP-Seite an eine andere weitergeben?
    13. Wie kann ich eine PHP-Präsentation auf CD brennen?
    14. Werden meine PHP-Seiten von einer Suchmaschine indiziert?
    15. Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?
    16. Wie kann ich die Ausgabe meines Scriptes in einen anderen Frame umlenken?
    17. Wie kann ich mit PHP die Bildschirmauflösung des Browsers herausfinden?
  6. Typen und Funktionen
    1. Welche Variablenarten gibt es in PHP und wie greife ich auf sie zu?
    2. Welche Datentypen gibt es in PHP?
    3. Was muss ich bei der automatischen Typ-Konvertierung beachten?
    4. Wie schreibe ich eine Funktion mit einer variablen Anzahl von Argumenten?
    5. Wie gebe ich mehrere Werte mit einer Funktion zurück?
    6. Wie schreibe ich ein Script, das beliebige Parameter verarbeitet?
    7. Variable Variablen
    8. Was ist der Unterschied zwischen isset() und einem Vergleich auf den Leerstring?
    9. Wie kann ich JavaScript-Funktionen aus PHP heraus aufrufen?
    10. Wie kann ich PHP-Funktionen aus JavaScript heraus aufrufen?
    11. Wie heißt die Variable, die ich suche?
  7. Stringfunktionen
    1. Was ist besser, print() oder echo?
    2. Wie zerlege ich einen String?
    3. Wie zerlege ich eine URL?
    4. Wie gebe ich eine Zahl formatiert aus?
    5. Wie kann ich Zeilenumbrüche verarbeiten?
    6. Wie kann ich Zeilenumbrüche in <br> umwandeln?
    7. Wie breche ich einen String nach x Zeichen um?
    8. Wie kann ich einen String kürzen?
    9. Wie kann ich einen String als PHP-Code ausführen?
  8. Reguläre Ausdrücke
    1. Wie kann ich mehr über Reguläre Ausdrücke lernen?
    2. Soll ich ereg() oder preg() verwenden?
    3. Wie verwende ich die preg()-Funktionen?
    4. Was sind Reguläre Ausdrücke?
    5. Welche Bauelemente kommen in Regulären Ausdrücken vor?
    6. Wie teste ich auf die Existenz mehrerer Suchworte in einem String?
    7. Wie isoliere ich Suchstrings aus einem größeren Text?
    8. Wie finde ich alle Links in einer HTML-Datei?
    9. Wie ersetze ich alle relativen Links in einer HTML-Datei?
    10. Wie überprüfe ich einen String auf seinen Inhalt?
    11. Wie ersetze ich in einem Text, jedoch nicht innerhalb von HTML-Tags?
    12. Wie mache ich aus URIs im Text anklickbare Links?
    13. Hilfe, mein Regulärer Ausdruck frißt zuviel!
    14. Wie ersetze ich alle relativen Links in einer HTML-Datei durch absolute?
  9. Arrays und Arrayvariablen
    1. Wie kann ich ein Element an ein Array anfügen?
    2. Wie kann ich ein Array aufzählen?
    3. Wie kann ich ein Element aus einem Array löschen?
    4. Wie greife ich auf ein mehrdimensionales Array zu?
    5. Wie kann ich ein Array nach einem beliebigen Kriterium sortieren lassen?
    6. Wie kann ich Duplikate aus einem Array entfernen?
    7. Wie kann ich ein Array von einer Seite auf eine andere transportieren?
  10. Klassen und Objekte
    1. Warum Klassen und Objekte benutzen?
    2. Wie definiere ich eine Klasse? Wie erzeuge ich ein Objekt?
    3. Was ist $this?
    4. Was ist extends? Was ist Vererbung?
    5. Was ist ein Konstruktor?
    6. Was sind polymorphe Funktionen? Kann ich sie simulieren?
    7. Wie kann ich Metainformationen über eine Klasse bekommen?
    8. Wie speichere ich ein Objekt in einer Session?
    9. Wie komme ich an den Namen eines Objekts?
  11. Variablen und Formulare
    1. Wie übergebe ich Variablen aus einem Formular an ein PHP-Script?
    2. Wie kann ich ohne Formular Variablen an ein Script übergeben?
    3. Wie viele Formularelemente kann ich auf einer Seite haben?
    4. Sollte ich besser GET oder POST verwenden?
    5. Wie verarbeite ich ein <input type="text">-Feld?
    6. Wie verarbeite ich eine Textarea?
    7. Wie kann ich aus einer Datenbanktabelle einen <select> erzeugen?
    8. Wie kann man ein <select multiple> verarbeiten?
    9. Wie kann man Radio-Buttons verarbeiten?
    10. Wie kann man Checkboxen verarbeiten?
    11. Wie funktioniert ein Datei-Upload über HTML-Formulare?
    12. Wie kann ich mehrere Dateien auf einmal uploaden?
    13. Wie erfahre ich den Status während eines Datei-Uploads?
    14. Wie verarbeite ich <input type="image">?
    15. Wie erkenne ich den Klick auf einen Submit-Button?
    16. Wie verarbeite ich mehrere Submit-Buttons?
    17. Wie verarbeite ich einen Reset-Button?
    18. Wie erkenne ich fehlerhafte/fehlende Eingaben?
    19. Wie verhindere ich mehrfaches Absenden eines Formulars?
    20. Warum funktionieren meine Formulare nicht?
  12. Sicheres Programmieren in PHP
    1. Wie unterscheide ich böse Variablen von guten?
    2. Was genau bewirkt safe_mode und ist das sicher?
    3. Warum ist es schlecht, mit dem Referer zu arbeiten?
    4. Für welche Zwecke ist der Referer zu gebrauchen?
    5. Kann ich PHP-Dateien kompilieren und so vor Dritten schützen?
    6. Apache: Wie kann ich ein Verzeichnis mit einem Passwort schützen?
    7. Apache: Wie kann ich ein Verzeichnis mit PHP mit einem Passwort schützen?
    8. Kann ich mit CGI PHP ein Verzeichnis mit einem Passwort schützen?
    9. Wie kann ich Passwörter sicher speichern?
    10. Vermeide globale Variablen
    11. Prüfe importierte Parameter. Traue niemandem
    12. Was sind Race Conditions? Wie kann ich sie vermeiden?
  13. Dateifunktionen und Programmausführung
    1. Wie kann ich eine Datei auslesen?
    2. Wie kann ich ein externes Programm von PHP aus starten?
    3. Wie realisiere ich einen Dateidownload mit PHP?
    4. Wie kann ich in einer Datei eine Zeile einfügen oder löschen?
    5. Wie kann ich eine Datei zeilenweise rückwärts auslesen?
    6. Wie kann ich einen Datei-Upload per FTP durchführen?
    7. Unix: Welche Zugriffsrechte brauche ich, um eine Datei anzulegen?
    8. Wie kann ich mit PHP auf die serielle Schnittstelle zugreifen?
    9. Warum funktioniert unlink() unter Windows nicht?
    10. Wie wende ich include() mit verschachtelten Verzeichnissen an?
    11. Wie übergebe ich Variablen an eingebundene Dateien?
  14. Datums- und Kalenderprobleme
    1. Wie kann ich das aktuelle Datum bekommen?
    2. Wie kann ich ein deutsches Datum in MySQL-Format umwandeln (und umgekehrt)?
    3. Wie kann ich die Anzahl der Tage zwischen zwei Daten bestimmen?
    4. Wie kann ich das Datum des Vortages bestimmen?
    5. Wieviel Tage hat der aktuelle Monat?
    6. Wie kann ich die Datumsausgabe auf Deutsch umstellen?
  15. Mail lesen und schreiben
    1. Was ist SMTP?
    2. Was ist das Domain Name System?
    3. Unix: Wie funktioniert der Mailversand?
    4. Windows: Wie funktioniert der Mailversand?
    5. Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?
    6. Wie kann ich eine HTML-Mail versenden?
    7. Wie kann ich ein Attachment mit einer Mail versenden?
    8. Wie kann ich eine Mail effizient an sehr viele Empfänger versenden?
    9. Wie kann ich die Gültigkeit einer Mailadresse testen?
    10. Wie kann ich überprüfen, ob eine versendete Mail tatsächlich angekommen ist?
    11. Wie kann ich feststellen, ob eine Mailadresse äußerlich gültig ist?
    12. Wie versende ich SMS mit PHP?
    13. Wie kann ich den Absender meiner Mail festlegen?
  16. Datenbanken
    1. Wie kann ich mehr über SQL lernen?
    2. Wieso kann ich mehrere, durch Semikolon getrennte Statements nicht ausführen?
    3. Ist es sinnvoll, Bilder in einer Datenbank abzulegen?
    4. Windows: Jeder Zugriff auf meine Datenbank dauert eine halbe Minute!
    5. Wie kann ich meine Datenbankperformance steigern?
    6. Wie kann ich zwei Tabellen miteinander verknüpfen?
    7. Was ist Aggregation? Was ist GROUP BY?
    8. Was ist der Unterschied zwischen connect und pconnect?
    9. Wie kann ich mein Datenbankpasswort gegen Spionage sichern?
    10. MySQL oder PostgreSQL?
    11. Wie komme ich bei meinem Provider an die Datenbank?
    12. Wie kann ich auf einen ODBC-Server (MSSQL, Access) zugreifen?
    13. Wieso wird aus " plötzlich \" und wie geht das wieder weg?
    14. Warum soll ich nicht SELECT * schreiben?
    15. Wie bekomme ich den letzten Datensatz aus der Tabelle?
    16. Meine IDs haben Lücken - wie vergebe ich sie neu?
    17. Meine Datenbankabfrage funktioniert nicht
    18. Wie kann ich bösartigen Code in SQL-Abfragen unterbinden?
  17. Datenbanken: MySQL
    1. Kommt MySQL mit mehr als x Datensätzen pro Tabelle klar? Wie stabil ist MySQL?
    2. Wie greife ich auf eine MySQL-Datenbank zu?
    3. Wie kann ich eine CSV-Datei in MySQL importieren?
    4. Wie kann ich eine CSV-Datei aus MySQL exportieren?
    5. Wie kann ich die Datensätze der letzten 2 Wochen listen?
    6. Wie kann ich eine Tabelle nach IP-Nummern sortieren lassen?
    7. Wie lösche ich alle Datensätze, die älter als n Tage sind?
    8. Wie kann ich Bilder in einer MySQL-Datenbank speichern?
    9. Wie kann ich einen zufälligen Eintrag aus einer MySQL-Tabelle auswählen?
    10. Ich habe eine Tabelle mit n Einträgen und möchte auf jeder Seite m davon anzeigen
    11. Wozu ist auto_increment nützlich? Wie erfahre ich den Wert des letzten Inkrements?
    12. Wie lege ich den Initialwert des auto_increment fest? Läuft dieser Wert über?
    13. Wie realisiere ich eine Volltextsuche mit MySQL?
    14. Meine Datenbankabfrage/Mein SQL-Statement funktioniert nicht
    15. Wie kann ich Umlaute richtig sortieren?
    16. Wie kann ich mehrere Verbindungen zu MySQL öffnen?
    17. Wie kann ich feststellen wie viele Datensätze von meiner Abfrage betroffen sind / gefunden wurden?
  18. Datenbanken: Oracle
    1. Ora oder OCI?
    2. Ich habe Oracle-Support mit --with-oci8 in PHP eincompiliert, nun startet der Apache nicht mehr.
    3. Der Webserver verbraucht jetzt viel mehr Speicher als ohne Oracle, mache ich was falsch?
    4. Umlaute, die in die Datenbank eingetragen wurden, werden nicht korrekt dargestellt.
    5. Gibt es auto_increment unter Oracle?
    6. Ich verwende das obige Beispiel. Wie kann ich nun mysql_insert_id() emulieren?
    7. Wie selektiere ich nur bestimmte Zeilen (LIMIT unter MySQL)?
    8. Wie speichere ich Datensätze mit mehr als 2000 Zeichen ab?
    9. Wie bearbeite ich LOBs mit PHP?
    10. Wie nenne ich Spalten um?
    11. Wie kann ich SQL Skriptdateien in Oracle ausführen?
    12. Welche freien Tools gibts für Oracle?
    13. Ich bekomme ein Oracle Fehlernummer ORA-XXXXX, wo stehen die Fehlercodes?
    14. Welche Bücher zu Oracle sind empfehlenswert?
  19. Datenbanken: Sybase
    1. Sybase-DB oder Sybase-CT?
    2. FreeTDS
    3. String-Quoting bei Sybase
    4. MySQL-Kompatibilität: Tabellen auflisten
    5. MySQL-Kompatibilität: Datenbanken auflisten
    6. MySQL-Kompatibilität: Server-Info
    7. MySQL-Kompatibilität: Prozesse auflisten
    8. Sybase: Changed database context...
    9. Hostnamen definieren
    10. Textfelder
  20. Datenbanken: Microsoft SQL Server
    1. SQL-Benutzer kann sich nicht anmelden
    2. Kein mssql_affected_rows in PHP vorhanden
    3. Nach Textfeldern sortieren
    4. Sonderzeichen " und ' werden nicht korrekt escaped
    5. Mein Spaltenname ist länger als 32 Zeichen und mssql_fetch_array liefert einen leeren String
    6. MySQL-Kompatibilität: Tabellen auflisten
    7. MySQL-Kompatibilität: Datenbanken auflisten
    8. MySQL-Kompatibilität: Server-Info
    9. MySQL-Kompatibilität: Prozesse auflisten
  21. phpMyAdmin
    1. Was ist phpMyAdmin?
    2. Ich bin kein MySQL-Administrator. Wie kann ich phpMyAdmin nur für mich selbst installieren?
    3. Ich bin MySQL-Administrator und möchte ein Exemplar phpMyAdmin für alle meine User installieren.
    4. Wieso kann ich den Inhalt meiner Tabelle nicht editieren?
    5. Wieso werden TIMESTAMP-Felder nicht auf die aktuelle Zeit gesetzt, wenn ich eine neue Zeile einfüge?
    6. Wieso kann ich in phpMyAdmin mehrere durch Semikolon getrennte SQL-Statements ausführen, nicht aber mit normalen PHP-Funktionen?
    7. Fehler: Die zusätzlichen Funktionen für verknüpfte Tabellen wurden automatisch deaktiviert.
    8. "Das $cfg['PmaAbsoluteUri']-Verzeichnis MUSS in Ihrer Konfigurationsdatei angegeben werden!"
  22. Grafikfunktionen
    1. Wie kann ich Thumbnails von einer Webseite erzeugen lassen?
    2. Wie kann ich mit PHP Diagramme erstellen?
    3. Wie kann ich Bilder verkleinern?
    4. Warum werden beim Bearbeiten von Bildern mit den Image-Funktionen die Farben verfälscht?
  23. PDF-Dateien
    1. Kann ich PDF-Dateien mit PHP erstellen?
    2. Welche Maßeinheit wird im PDF-Format verwendet?
    3. Kann ich für Höhe und Breite unterschiedliche Maßstäbe verwenden?
    4. Kann ich bestehende PDF-Dateien als Template für dynamische Dokumente verwenden?
    5. Kann ich meine PDF-Dateien irgendwie schützen?
    6. Gibt es eine Rechteverwaltung für PDF-Dateien?
    7. Wie erzeuge ich geschütze PDF-Dateien?
    8. Kann ich sicher feststellen, ob meine Besucher PDF-Dateien lesen können?
    9. Welchen MIME-Type muss ich für PDF-Dateien verwenden?
    10. Wie sehe ich, ob meine PHP-Installation die PDF-Bibliotheken benutzen kann?
    11. Wie mache ich die PDF-Bibliotheken für meine Installation verfügbar?
    12. Warum stellt mein Internet Explorer statt der PDF-Datei eine leere Seite dar?
  24. Content Management Systeme
    1. Was ist ein Content Management System? Warum ist es nützlich?
    2. Welche PHP-basierten Content Management Systeme gibt es?
  25. Häufig benötigte Codeschnipsel
    1. Wie kann ich eine schummelsichere Abstimmung codieren?
    2. Wie kann ich einen HTTP POST-Request absenden?
    3. Wie kann ich einen HTTP POST-Request mit Datei-Upload absenden?
    4. Wie kann ich die IP des Users erfahren?
    5. Wie kann ich die Performance zweier Befehle vergleichen?
    6. Wie kann ich den Inhalt eines Verzeichnisses samt dem Inhalt aller Unterverzeichnisse ausgeben?
    7. Wie kann ich aus einem Zahlenbereich von x bis y, zufällig n Zahlen auswählen, so dass keine Zahl doppelt vorkommt?
    8. Wie kann ich zählen, wie oft auf einen Link geklickt wurde?
    9. Wie kann ich das Datum der letzten Änderung einer Datei erfahren?
    10. Wie biete ich meine Seiten mehrsprachig an?
    11. Wie kann ich ermitteln, wieviele Besucher gerade meine Seite betrachten?
    12. Wie überprüfe ich Hyperlinks auf ihre Gültigkeit?
    13. Wie erzeuge ich Excel-Dateien mit PHP?
    14. Wie kann ich prüfen, ob eine IP-Adresse in einem bestimmten Bereich liegt?
    15. Wie überprüfe ich, ob eine Zahl gerade oder ungerade ist?
    16. Wie wandle ich Sekunden in Tage/Stunden/Minuten/Sekunden um?
    17. Wie stelle ich Tabellenzeilen abwechselnd farbig dar?
    18. Wie kann ich prüfen, ob eine bestimmte ICQ UIN online ist?
    19. Wie kann ich prüfen, ob eine bestimmter Yahoo! Messenger User online ist?
    20. Wie kann ich das Alphabet aufzählen?
  26. Häufig nachgefragte Standardscripte
    1. Wo finde ich ein Script, das "xyz" kann?
    2. Wie kann ich eine Volltextsuche realisieren?
    3. Wie kann ich mit PHP News lesen und schreiben?
    4. Wie kann ich einen Onlineshop mit PHP realisieren?
    5. Welche in PHP realisierte Foren gibt es?
    6. Welche Webmail-Oberflächen in PHP gibt es?
    7. Welche kostenlose Portalsoftware in PHP gibt es?
    8. Welche Lösungen gibt es, um Diagramme zu erstellen?
    9. Wie kann ich ID3-Tags von MP3-Dateien lesen/schreiben?
    10. Wie kann ich eine whois-Abfrage mit PHP realisieren?
    11. Welche Groupware-Tools in PHP gibt es?
  27. Guter Code
    1. Halte Code links. Verwende Wächter statt Schachtel-if
    2. Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform
    3. Trenne Aussehen und Inhalt
    4. Was sind eigentlich if-Schleifen?
    5. Anführungzeichen oder Hochkomma?
    6. Mein Script funktioniert nicht mit Browser XY!
    7. Schaden Kommentare der Performance?
    8. Wie kann ich das Caching einer Seite verhindern?
    9. Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?
    10. Wie kann ich mit PHP WAP-Seiten erzeugen?
  28. Häufige Fehlermeldungen
    1. Was ist das für ein @-Zeichen vor einigen Funktionsaufrufen?
    2. Warning: Supplied argument is not a valid File-Handle resource
    3. Fatal error: Maximum execution time exceeded
    4. Supplied argument is not a valid MySQL result...
    5. Windows: Call to unsupported or undefined function: xy()
    6. Unix: Call to unsupported or undefined function: xy()
    7. Warning: xy() is not supported in this PHP build
    8. MySQL-Server has gone away
    9. Unix: Call to unsupported or undefined function: OCILogon()
    10. Warning: ORA-12154: TNS:could not resolve service name
    11. Warning: ORA-12705: invalid or unknown NLS parameter value specified
    12. Warning: Cannot send session cookie - headers already sent ...
    13. Warning: Cannot add header information - headers already sent ...
    14. Warning: Wrong parameter count for xy()
    15. Notice: Use of undefined constant ...
    16. Notice: Undefined variable ...
    17. Warning: Failed opening 'xy' for inclusion ...
    18. Parse error: parse error in ...
    19. 0 is not a MySQL result index
    20. Warning: Unknown error ...
    21. Warning: Failed to Connect ...
    22. Warning: open(/tmp\sess_..., O_RDWR) failed ...
    23. Warning: fopen() - No such file or directory
    24. Warning: fopen() - No error
    25. Document contains no data
    26. Parse error on line 1 ... (bei Verwendung von XML/XHTML)
    27. Fatal error: Cannot redeclare class ... in ... on line ...
    28. Fatal error: Cannot redeclare ... (previously declared in ...) in ... on line ...
  29. Sessions
    1. Wie realisiere ich Sessions mit PHP?
    2. Was ist eine Session-ID? Was ist PHPSESSID?
    3. Wie stelle ich fest, ob der Client die Cookie-Annahme verweigert?
    4. Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback?
    5. Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen?
    6. Wie schütze ich Sessiondaten zusätzlich?
    7. Wie groß darf die Menge an Daten sein, die ich in einer Session speichern darf?
    8. Wie kann ich mir den Inhalt der Sessiondaten anzeigen lassen?
    9. Wie kann ich mir den Inhalt der Cookiedaten anzeigen lassen?
    10. Was geschieht im Filesystem des Servers, wenn ich Sessions benutze?
    11. Wie benutze ich die Session-Funktionen unter Windows?
    12. Was sind Sessions und warum sind sie nützlich?
    13. Wie speichere ich Objekte in Sessions?
    14. Soll die Session-ID in URL-Parametern oder Cookies gespeichert werden?
    15. Warum verwendet PHP nicht die IP-Nummer des Browsers als Schutz gegen eine Übernahme der Session?
    16. Wie kann ich Reloads durch den User erkennen und verhindern?
  30. PEAR
    1. Was ist PEAR?
    2. Wo kann ich PEAR downloaden?
    3. Wie installiere ich PEAR?
    4. Wie nutze ich den PEAR Installer?
    5. Wo finde ich weitere Informationen zu PEAR?
  31. Open Publication License
    1. Englische Version
    2. Deutsche Version
  32. PHP 3
    1. Wie funktioniert ein Datei-Upload über HTML-Formulare bei PHP 3?
    2. Ich verwende PHP (Version 3) als Apache-Modul. Wie kann ich dies konfigurieren?
    3. Wie kann ich Duplikate aus einem Array entfernen?

1. Über diese FAQ

1.1. Was ist das hier?

Keywords: dclp FAQ | Einfuehrung

Antwort von Kristian Köhntopp

Dies ist die FAQ (FAQ = Frequently Asked Questions [Häufig gestellte Fragen]) für die Newsgruppen de.comp.lang.php.* (de.comp.lang.php.misc, de.comp.lang.php.datenbanken, de.comp.lang.php.installation, und de.comp.lang.php.netzprotokolle) Sie erklärt den Zweck der Newsgruppen, auf welche Weise man hier am einfachsten an sinnvolle Antworten kommt und dient als Sammlung von Antworten auf häufig gestellte Fragen in der Gruppe.

Wenn Du Kommentare oder Vorschläge zu diesem Artikel hast oder wenn Du selber einige Abschnitte in diesen Artikel einbringen möchtest, wendest Du Dich am besten per Mail an die Mailingliste zur de.comp.lang.php FAQ, <faqlist@php3.de>.

1.2. Wie ist die Charta dieser Newsgroup?

Antwort von Clemens Koppensteiner

de.comp.lang.php.datenbanken

Hier geht es um alle Fragen, die bei der Verwendung von PHP mit den verschiedensten Datenbanken wie MySQL, PostgreSQL, Oracle, MSSQL etc. auftreten können. Fragen zur Datenbank selbst müssen in der jeweiligen Datenbank-Gruppe gestellt werden.

de.comp.lang.php.installation

Hier geht es um alle Fragen, die bei der Installation und Konfiguration von PHP auftreten können. Hierzu gehören insbesondere Fragen zur Kompilation und Integration für die von PHP unterstützten Webserver. Hinzu kommen Fragen zur Installation von div. PHP-Erweiterungen, z. B. für Datenbanken.

de.comp.lang.php.netzprotokolle

Hier geht es um alle Fragen, die bei der Programmierung mit sowie der Umsetzung von Netzprotokollen wie HTTP, FTP, Mail&News etc. mit PHP auftreten können.

de.comp.lang.php.misc

Diese Newsgruppe richtet sich an alle Benutzer und Programmierer von PHP, einer Scriptsprache mit Schwerpunkt auf der Entwicklung von Webanwendungen.

Es können alle Themen rund um PHP besprochen werden, die nicht durch eine der anderen de.comp.lang.php.* Gruppen abgedeckt werden.

Antwort von Kristian Köhntopp

Die Hierarchie de.comp.lang.php.* entstand im November 2002 durch Aufteilung der Newsgroup de.comp.lang.php. Die Aufteilung wurde notwendig, weil der Verkehr in de.comp.lang.php so groß wurde, dass ein sinnvolles Arbeiten in der Newsgroup nicht mehr möglich war.

de.comp.lang.php wurde im Januar 2000 eingerichtet und im November 2002 gelöscht und durch de.comp.lang.php.* ersetzt.

1.3. Wo finde ich die aktuelle Version dieser FAQ?

Keywords: dclp FAQ | Version | Download

Antwort von Kristian Köhntopp

Eine Downloadversion dieser FAQ im HTML-Format findet sich unter der URL http://www.php-faq.de/faq-html.tar.gz.

Windows-Anwender können die FAQ auch im CHM-Format laden, das in der jeweils aktuellsten Version unter der Adresse http://www.php-faq.de/dclp-faq.chm abgelegt ist. (Zum Ansehen von CHM-Dateien ist eine hinreichend neue Version des Microsoft Internet Explorer notwendig).

Die aktuelle Version dieser FAQ ist unter der URL http://www.php-faq.de zu lesen. Eine Version in einer einzigen Datei befindet sich unter der URL http://www.php-faq.de/faq-single.html.

Die XML-Quelltexte dieser FAQ sind unter der URL http://www.php-faq.de/faq.tar.gz zu finden.

1.4. Kann ich eine Kopie der FAQ per Mail zugesendet bekommen?

Antwort von Kristian Köhntopp

Die FAQ wird nicht als Mail versendet. Die Frage Wo finde ich die aktuelle Version dieser FAQ? beschreibt, wie und in welchen Formaten die FAQ bezogen werden kann.

1.5. Du hast doch für die FAQ geschrieben. Ich habe da eine Frage zu PHP...

Keywords: Hilfe

Antwort von Kristian Köhntopp

Der Autor dieser Antwort erhält zur Zeit pro Woche zwischen 40 und 60 private Fragen nach Hilfe zu PHP. Keine dieser Fragen wird beantwortet - das ist arbeitsmäßig einfach nicht zu schaffen.

Allgemein: Es ist sinnlos, Fragen per Mail an einen der Autoren dieser FAQ zu senden. Du belastest damit eine einzelne Person mit Arbeit, statt die Arbeit auf die Newsgroup zu verteilen. Außerdem ist diese Arbeit verschwendet, denn die Antwort wird nur von Dir und nicht von den anderen Lesern der Newsgroup gelesen. Auch kann die Antwort nicht vom FAQ-Team weiterverarbeitet werden.

Keiner der Autoren der FAQ wird Dir privaten Support per Mail leisten. Stelle Deine Frage bitte in der entsprechenden Newsgroup. Einen Überblick über die PHP-Newsgroups findest du hier: Wie ist die Charta dieser Newsgroup?.

1.6. Darf ich die FAQ auf meiner Site einbinden? Darf ich sie für meine Zwecke verwenden?

Keywords: Copyright

Antwort von Kristian Köhntopp

Diese FAQ ist ein Gemeinschaftswerk der genannten Autoren. Sie ist wie alle Werke urheberrechtlich geschützt, und kann unter den Bedingungen der Open Publication License, Englische Version genutzt werden. Die unter Open Publication License, Deutsche Version bereitstehende Übersetzung der Lizenz ist inoffiziell und nicht bindend.

Eine Zustimmung der Autoren zur Wiederveröffentlichung ist im Rahmen dieser Lizenz nicht erforderlich. Die Lizenz erlaubt Ihnen, den Text der FAQ beliebig zur Gänze oder in Teilen zu verwenden, solange die Namen der Autoren erhalten bleiben und an prominenter Stelle genannt werden. Die Lizenz ist unwiderruflich und bindend, d.h. die Autoren können die Lizenz weder entziehen noch verändern, solange Sie die Lizenzbedingungen einhalten.

Von der dclp-FAQ gibt es einige Mirrors. Wir haben mir diesen Mirrors in der Vergangenheit schlechte Erfahrungen gemacht: Die meisten dieser Mirrors bauen den Build-Prozeß der FAQ nicht nach und veröffentlichen so eine veraltete Version der FAQ. Ein besonders abschreckendes Beispiel ist die Kopie der FAQ auf dem PHP-Center, die dortige Version der FAQ ist nun schon seit Juli 2001 nicht mehr aktualisiert worden und die dort enthaltenen Informationen sind inzwischen gefährlich und schädlich.

Wir können und wollen Ihnen eine Kopie des Materials in Teilen oder zur Gänze nicht untersagen. Wir bitten Sie aber nachdrücklich, sich zu überlegen, ob es nicht günstiger wäre, stattdessen einen Link auf die Originalseite bei uns zu setzen. Die FAQ ist ein lebendiges Projekt, und sie verändert sich. Wenn Sie den Build-Prozeß der FAQ nicht für sich nachstellen können und die Informationen der FAQ nicht automatisch aktualisieren können, dann generieren sie bloß eine weitere veraltete Kopie der FAQ und helfen nicht wirklich dabei, die Welt in einen besseren Ort zu verwandeln.

Die FAQ wird derzeit auf eine Art und Weise gehostet, die ein Verschwinden der FAQ durch Trafficprobleme oder Servermangel sehr unwahrscheinlich macht ("sponsored by schlund+partner", Schlund stellt uns und dem deutschen Mirror des PHP-Manuals einen Rootserver zur Verfügung). Desweiteren können wir seit einiger Zeit stabile URLs garantieren und es ist ein Designziel der FAQ, Erklärungen unter unveränderlichen URLs zu präsentieren.

Wir laden Sie also herzlich ein, die FAQ zu verlinken statt eine weitere tote Kopie zu erzeugen, oder alternativ den Buildprozeß der FAQ bei sich zu installieren (sie benötigen CVS, Sablotron und Make sowie ggf. ssh). Letzteres würde Ihnen auch ermöglichen, schreibenden Zugriff auf die FAQ zu bekommen und Inhalte mitzugestalten.

Ihr Ansprechpartner in allen Fragen die dclp-FAQ als Ganzes oder in Teilen betreffend ist faqlist@php3.de, die Mailingliste der deutschen dclp-FAQ. Sie erreichen dort alle Autoren der FAQ sowie weitere an der Gestaltung der FAQ interessierte Personen.

1.7. Was soll ich tun, wenn ich einen Fehler in der FAQ gefunden habe?

Antwort von Martin Jansen

Wenn Du einen Fehler in einem der Texte der FAQ gefunden hast, dann bitten wir Dich, uns diesen mitzuteilen. Dazu schickst Du am besten eine E-Mail an faqlist@php3.de. Unter dieser Adresse erreichst Du die Mailingliste der de.comp.lang.php-FAQ, welche alle Autoren der FAQ abonniert haben.

Wenn du einen Fehler in der FAQ gefunden hast, dann kannst Du ihn direkt in den Dateien ändern, die du via CVS heruntergeladen hast. Anschließend kannst Du die Änderungen in eine Datei schreiben und diese dann an die dclp-FAQ Mailingliste schicken:

# Änderungen in Datei umleiten
$ cvs diff -u > /tmp/diff

# E-Mail an dclp-FAQ Mailingliste schicken (Beispiel-Programm: mutt)
$ mutt -a /tmp/diff -s 'Meine Aenderungen an der FAQ' faqlist@php3.de

1.8. Kann ich selber für diese FAQ schreiben?

Keywords: Mailingliste | german-faq | CVS

Antwort von Kristian Köhntopp

Ja, sofern Du die in Open Publication License beschriebene Lizenz für Dich akzeptieren kannst. Diese Lizenz bedeutet im wesentlichen, dass Du an Deinen eigenen Texten das volle Urheber- und Verwertungsrecht behältst, aber jedermann das Recht einräumst, die FAQ zu nutzen und unverändert und mit Hinweis auf die Originalquelle und die Originalautoren zu reproduzieren.

Rein technisch benötigst Du die folgenden Utensilien:

  • Einen Rechner mit Texteditor und PHP zum Testen.

  • Einen CVS-Client.

  • optional einen beliebigen XSLT-Prozessor (z.B. Sablotron).

Es gibt eine Mailingliste faqlist@php3.de, die Nachrichten über Änderungen an der FAQ enthält und bei der man Hilfe für Autoren bekommt. Man kann die Mailingliste unter der Adresse faqlist-subscribe@php3.de bestellen.

Es existiert ein CVS-Archiv, aus dem die aktuelle Version der FAQ bezogen werden kann. Die CVSROOT dieses Archives ist :pserver:anonymous@php3.de:/home/cvs/repositories/php (ohne Passwort). Das Modul heißt phpfaq.

$ cvs -d :pserver:anonymous@php3.de:/home/cvs/repositories/php login
Password: 
$ cvs -d :pserver:anonymous@php3.de:/home/cvs/repositories/php checkout phpfaq
...

# Aktualisieren der Version mit
$ cd phpfaq
$ cvs -z9 update -dAP

Diese Version ist immer aktueller als die auf dem Webserver veröffentlichte Version.

Wenn du deine Änderungen an die Mailinglist faqlist@php3.de schicken willst, hänge bitte ein diff als Attachment an dein Mail an.

$ cvs diff -u > /tmp/diff

# mit 'mutt' ein E-Mail an die Liste schicken
$ mutt -a /tmp/diff -s 'Mein Diff' faqlist@php3.de

1.9. Soll ich Jobangebote in de.comp.lang.php.misc posten?

Keywords: Spam | dmabi

Antwort von Kristian Köhntopp

Eine kurze Umfrage im Januar 2000 in de.comp.lang.php hat ergeben, dass Jobangebote in der Newsgroup toleriert werden - auch wenn sie nach Charta streng genommen off-topic sind - solange sie folgenden Ansprüchen an die äußere Form genügen:

  • Jobangebote sollen im Betrefftext des Artikels die Kennzeichnung [JOB] haben. Auf diese Weise sind sie leicht erkennbar und können von den Leuten, die sie nicht sehen wollen leicht unterdrückt werden, während sie von den Leuten, die einen Job suchen, leicht gefunden werden.

  • Jobangebote sollten mit der Headerzeile Followup-To: poster veröffentlicht werden. de.comp.lang.php.misc nimmt die Veröffentlichung von Jobangeboten hin, ist aber nicht zur Diskussion über Jobangebote gedacht.

  • Jobangebote sollten nicht übermäßig oft veröffentlicht werden: Jeder Job sollte nur genau einmal angepriesen werden und Arbeitgeber mit ständigen oder wiederkehrenden Jobangeboten sollten nicht öfter als einmal im Monat veröffentlichen. Es hat keinen Sinn sich aufzudrängen, und bei einem unbeliebten Arbeitgeber wird in der jetzigen Arbeitsmarktsituation wohl kaum jemand anfangen.

  • Jobangebote sollten netiquettekonform sein: Der Absendername soll eine Person ("Paul Arbeitgeber") und keine Funktion ("Personalbüro Arbeitgeber GmbH") sein. Die angegebene Mailadresse soll gültig sein. Im Newsartikel soll kein HTML verwendet werden und Netscape Visitenkarten sollen nicht verwendet werden; Hochglanz-Blendwerk ist nett für das Marketing, aber wir sind R&D.

  • Jobangebote können in die formal korrekte Newsgroup de.markt.arbeit.biete.it-berufe crosspostet werden. Dann ist es doppelt wichtig, dass ein Followup-To: poster oder Followup-To: de.markt.arbeit.d gesetzt wird. de.comp.lang.php.* ist nicht zur Diskussion von Jobangeboten geeignet!

Wenn Sie als Arbeitgeber nicht in der Lage sind, im USENET intelligent, kooperativ und regelkonform aufzutreten, sollten Sie andere Medien für Ihre Personalaquise verwenden, die sich Ihnen leichter erschließen. Ihre Corporate Identity wird es Ihnen danken.

1.10. Wer kann mir einen Provider empfehlen?

Antwort von Kristian Köhntopp

Eine kurze Umfrage im Februar 2000 in de.comp.lang.php hat ergeben, dass Fragen nach Providern oder Providerspezifika in dieser Newsgroup nicht willkommen sind. Die korrekte Newsgroup für diese Frage ist de.comm.provider.webspace.

Eine Providerdatenbank wird unter anderem bei Dynamic Web Pages, beim PHP-Center und bei Webhostlist.de betrieben.

Ebenso unerwünscht sind providerspezifische Fragen wie Wie komme ich bei xyz an die MySQL-Datenbank?. Der korrekte Ansprechpartner für solche Fragen wäre der Support des betreffenden Providers bzw. dessen FAQ. Werden derartig providerspezifische Fragen dennoch in die Newsgroup gestellt, ist es höflich, Followup-To: poster zu setzen und hinterher eine Zusammenfassung der eingegangenen Mails zu posten.

1.11. Warum bekomme ich Ermahnungsmails, wenn ich Autoren in der Gruppe auf Netiquetteverstöße aufmerksam mache?

Keywords: Netiquette

Antwort von Kristian Köhntopp

Du hast vollkommen Recht: Manche Autoren in der Newsgroup verstoßen gegen die Netiquette, wie sie in de.newusers.infos gepostet wird. Sie tun dies etwa, indem sie ohne vollen Realnamen schreiben, inkorrekte Mailadressen ("nospam", "deletethis") angeben oder Artikel mit HTML oder Netscape-Visitenkarten versenden. Du sollst das auch nicht hinnehmen.

In einer Newsgroup ist der Ton jedoch genauso wichtig wie der Inhalt. Die Regulars von de.comp.lang.php sind stolz auf den freundlichen und hilfsbereiten Ton in ihrer Newsgroup. Wenn Du also einen anderen Autor an die Netiquette erinnern möchtest, dann tue dies bitte unbedingt per Mail und nicht öffentlich in der Gruppe. Auch die Netiquette, auf deren Einhaltung Du bestehst, fordert dies - Du kannst nicht auf der einen Seite auf der Einhaltung der Netiquette bestehen und andererseits selbst dagegen verstoßen, ohne Glaubwürdigkeit zu verlieren.

Und bitte: Halte Deinen Ton auch in der Mail freundlich. Du wirst leichter verstanden und erreichst das gewünschte Ziel viel eher.

Wenn Du meinst, Deinen Artikel dennoch öffentlich posten zu müssen, etwa um einen Autoren an die korrekte Newsgroup zu verweisen, oder weil die angegebene Mailadresse nicht erreichbar ist, oder weil sich der Autor per Mail nicht einsichtig zeigt und sich niemand sonst bisher darum gekümmert hat, dann halte Deinen Beitrag bitte freundlich und konstruktiv. Das bedeutet: Beantworte die gestellte Frage oder löse das Problem des Fragers so gut Du kannst und weise dann auf die Netiquette hin. Wenn Du zu dem Problem des Fragers nichts beizutragen hast, dann poste lieber gar nichts - oder schreibe eine Mail. Du bist nicht allein in der Gruppe und Du musst die Welt nicht selbst retten. Ein anderer, der antworten kann, wird antworten und dabei wahrscheinlich auch auf korrektes Verhalten hinweisen.

Regeldiskussionen gehören in die dafür vorgesehene Newsgroup, de.soc.netzkultur.umgangsformen, oder sollen mit einem Followup-To: poster versehen werden.

1.12. Warum bekomme ich Ermahnungsmails?

Keywords: Netiquette | TOFU | Pseudo

Antwort von Kristian Köhntopp

Du wirst nicht nur in de.comp.lang.php, sondern in den meisten anderen deutschen Newsgroups auf korrektes Verhalten in den Newsgroups hingewiesen, wenn Du ohne einen vollständigen Namen postest, Artikel ohne gültige Absenderadresse schreibst, Artikel mit Werbung absetzt, HTML oder Netscape-Visitenkarten in Deinen Artikeln versendest oder mutwillig Artikel in die falschen Newsgroups schreibst.

Diejenigen von uns, die schon länger in den USENET News aktiv sind, haben sich diese Regeln und Verhaltenformen nicht aus Spaß ausgedacht. USENET existiert schon seit mehreren Jahrzehnten und die Verhaltensnormen, auf deren Einhaltung bestanden wird, haben sich in langen Jahren entwickelt und bewährt. Es gibt einen guten Einführungstext aus de.newusers.infos mit dem Titel Warum soll ich mich an die Regeln halten? der erklärt, warum die Dinge so sind, wie sie sind.

Wenn Du von de.comp.lang.php Ergebnisse möchtest, also technische Hilfe bei Deinen Problemen mit der Scriptsprache PHP, dann tust Du gut daran, Deinen Texten auch eine akzeptable äußere Form zu geben.

1.13. Warum sind Flames sinnlos?

Keywords: Netiquette

Antwort von Kristian Köhntopp

Newbies, die sich nicht an geltende Netzkulturen halten oder schlecht formulierte Fragen stellen, kommen meist mit einem konkreten Problem nach de.comp.lang.php. Diese Leute bekommen dann allerdings häufig keine vernünftige Antwort, sondern werden mit Flames überhäuft. Der Grund liegt darin, dass beide Parteien mit unterschiedlichen Erwartungen und unterschiedlichen Kommunikationszielen in den Thread gegangen sind, und sie nicht bereit waren, von diesen Zielen abzuweichen. So ist keine sinnvolle Kommunikation zustande gekommen.

Eine sinnvolle Antwort auf ein schlecht formuliertes oder unhöfliches Posting unterscheidet sich in den folgenden Punkten:

Zunächst einmal versucht sie freundlich zu bleiben, ohne in der Sache nachzugeben.

Dann geht sie unmittelbar auf das Problem des Posters ein, d.h. sie hilft ihm auf eine konstruktive Weise, sein unmittelbares Problem zu lösen, um ihn wieder arbeitsfähig zu machen. Dies ist der wichtigste Aspekt der Nachricht aus der Sicht des Newbies oder Posters: Es ist egal, wie unsystematisch und offtopic die Nachricht von ihm oder Deine Antwort ist - wenn Du mit ihm etwas anfangen willst, musst Du zuerst seinen unmittelbaren Block lösen, damit Du sinnvolle Dinge nachschieben kannst.

Nachschieben heißt in diesem Zusammenhang, den Newbie mit weiterführenden Informationen zu versorgen, damit er mehr lernt, als er mit seiner Frage eigentlich bezweckt hatte. "Nachschieben" ist wichtig, denn nur so bekommt man Newbies schrittweise zu Regulars umgebaut.

Erst am Schluss eines Postings gibt es dann die Netiquette, quasi als Dressing obendrauf. Mit dem ganzen Zucker, der vorab geliefert worden ist, schmeckt das dann nicht mehr so bitter und dringt viel tiefer ein. Immerhin ist der Newbie ernst genommen worden und hat produktive Antworten bekommen, obwohl er sich mit seinem unerfahrenen Auftreten in de.comp.lang.php ziemlich lächerlich gemacht hat - das ist wie in Shorts und T-Shirt auf eine Sitzung mit lauter Anzügen und Schlipsen zu kommen: "Selbstverständlich können wir Ihnen die 10.000 Tonnen Schweinehälften liefern, und übrigens Herr Graczoll: Fällt Ihnen was an Ihrer Kleidung auf?"

Als Abschluss nocheinmal die Arbeitsschritte für guten technischen Support in de.comp.lang.php als Spickzettel:

  • Freundlich bleiben. Wenn Du nicht freundlich bleiben kannst, lass jemand anders die Arbeit machen. Wir sind genug Leute hier, Du musst die Welt nicht alleine retten. Und wenn Du ausbrennst, ist uns damit auch nicht geholfen.

  • Den Block wegräumen. Der Neuling kommt mit einem unmittelbaren Problem in die Gruppe. Räume dieses Problem weg. Wenn Du dieses Problem nicht lösen kannst, lass den Neuling in Ruhe. Jemand anders wird sich darum kümmern, Du musst die Welt nicht alleine retten.

    Bevor der Neuling nicht aufgemacht ist, kann man sekundäre Probleme nicht lösen. Auf den Neuling einzuschlagen, bevor er aufgemacht ist, ist kontraproduktiv und macht die Arbeit für andere nur schwieriger. Mache Deinen Kollegen die Arbeit nicht schwierig - wenn Du nicht aufmachen kannst, lass den Fall liegen.

  • Nachschieben. Ein Neuling ohne Block ist eine Gelegenheit. Nutze sie! Jetzt ist der Zeitpunkt gekommen, an dem Du die Welt retten kannst. Drück dem Newbie nach der unmittelbaren Antwort auf sein konkretes Problem noch eine Winzigkeit mehr rein, damit der arme Kerl das Licht sehen kann.

    Wenn Du ihm in 2. eine Query gebaut hast, zeig ihm Zusatzinfo zu SQL. Wenn er ein Problem mit den MySQL-Funktionen hatte, zeig ihm die passenden (nicht irgendwelche, die passenden!) Handbuchseiten. Wenn er ein Sicherheitsloch gebaut hatte, zeig ihm passende Zusatzinfo.

    Präsentiere diese Zusatzinfo so, dass dem Neuling der Mehrwert Deiner Antwort deutlich wird, und dass er motiviert ist, sich diese Information zu erarbeiten.

  • Geradebiegen. Wenn die Frage des Neulings Formfehler hatte, weise sachlich (!) und beiläufig auf diese Formfehler hin. Niemand will auf einer Party mit dem Megaphon ausgerufen werden: "Herr XYZ wird gebeten, den Hosenstall zu schließen." Andererseits will auch niemand den Nudelsketch von Loriot nachdrehen.

    Ergänze Deine Antwort wieder mit passenden URLs, etwa dem Abschnitt der FAQ, oder direkt mit den Links, die in der FAQ enthalten sind.

  • Wir helfen Dir. Du musst die Welt nicht alleine retten. Wir haben die FAQ speziell für Dich gebaut - mit der FAQ ist es einfacher und schneller für Dich, produktiv zu helfen, statt eine Flame zu schreiben: 30 Sekunden für eine entspannte Nachricht mit zwei FAQ-Zitaten statt mindestens vier Minuten Stress, um den Deppen manuell fertig zu machen.

1.14. Ich verwende Outlook Express und keiner hat mich lieb.

Keywords: TOFU | Microsoft | Netiquette

Antwort von Kristian Köhntopp

Das wird daran liegen, dass Du Dein Outlook Express nicht korrekt konfiguriert hast. Wahrscheinlich setzt Outlook Express nicht den korrekten Absendernamen, veröffentlicht Artikel in HTML oder in HTML- und Text-Versionen in doppelter Ausführung oder macht andere Dinge, die außer Microsoft niemand gut findet.

Bitte lies die Outlook Express FAQ, die für Deine Version von Outlook zutreffend ist und konfiguriere Deinen Newsreader korrekt. Auf OE 6 Step by Step gibt es eine bebilderte Anleitung, wie man mit Outlook richtige Quotezeichen einstellt und das proprietäre "AW:" in Antworten auf das richtige "Re:" umstellt.

Antwort von Kai Schröder

Leider waren den Programmierern von Outlook Express die vorhandenen Regeln im Usenet ein wenig egal. Dadurch wurden verschiedene Funktionen von OE so programmiert, dass du dir zwangsläufig damit Ärger einhandelst.

Zur Behebung der wichtigsten Fehlfunktionen in OE hat Christoph Hölken ein kleines Programm namens Outlook Express Tools (OET) programmiert. Du kannst damit in Zukunft technisch korrekte Postings ohne Kammquoting und überlange Zeilen erstellen und deine Signatur richtig abtrennen. Ausserdem wird beim Quoten der Cursor unter dem Original-Posting eingefügt, so dass es dir leichter fällt, TOFU (siehe Was ist TOFU? Wieso finden die Anderen meine Artikel schwer zu lesen?) zu vermeiden. Dieses Programm kannst du dir unter http://www.oe-tools.de.vu downloaden.

1.15. Was ist TOFU? Wieso finden die Anderen meine Artikel schwer zu lesen?

Keywords: Netiquette | TOFU

Antwort von Kristian Köhntopp

Text Oben, Fullquote Unten. Eine Unart, die einen nicht nur in dieser Newsgroup, sondern im ganzen Netz unbeliebt macht. Lies http://learn.to/quote/ von Dirk Nimmich, und speziell Abschnitt 2.3 "Warum soll ich meine Antwort nach dem Zitat plazieren?" und die folgenden.

1.16. Wie verweise ich auf die FAQ?

Antwort von Kristian Köhntopp

Ein Hinweis auf die FAQ sollte niemals einfach nur in der Form "RTFM" oder http://www.php-faq.de erfolgen, sondern immer auf eine konkrete Antwort zeigen. Der Leser sollte außerdem erfahren, welche Antwort er dort findet und wieso das Bezug zu der gestellten Frage hat. In der Newsgroup hat sich folgendes Format eingebürgert:

  1.21 Wie verweise ich auf die FAQ?
  http://www.php-faq.de/q/q-newsgroup-faqreferenz.html

Dieser Code befindet sich am Ende jeder Antwort, so dass er einfach kopiert werden kann.

1.17. Wie stelle ich meine Frage an die Newsgroup am sinnvollsten?

Antwort von Martin Jansen

Um in news:de.comp.lang.php.* eine sinnvolle Antwort zu erhalten, die Dir weiterhilft, solltest Du versuchen, Dich an die folgenden Regeln zu halten:

  • Lies möglichst zuerst in der Gruppe mit, um welche Themen es gerade geht. Nichts ist ätzender, als drei Mal am Tag dieselben Fragen lesen zu müssen. Recherchiere in den Newsgroup-Archiven bei www.emre.de oder groups.google.com nach deinem Problem - die meisten Fragen wurden schon gestellt und beantwortet.

    Zu fast allen PHP-Funktionen finden sich in den User Contributed Notes wertvolle Hinweise zur Verwendung, zu möglichen Stolperfallen, Plattformunterschieden etc.: www.php.net/[Funktionsname].

  • Konfiguriere Deinen Newsreader so, dass er in Deinen Postings Deinen richtigen Namen und Deine gültige E-Mail-Adresse anzeigt.

  • Stelle Deine Frage höflich und werden nicht ausfallend, auch wenn Dir nicht sofort jemand antwortet: Der Support, den Du im Usenet erhältst, ist im Gegensatz zu Hotlines etc. kostenlos und wird von anderen Leuten in Ihrer (teils knappen) Freizeit ohne eine Gegenleistung durchgeführt.

  • Damit Andere Dein Problem so gut wie möglich verstehen, solltest Du bei der Beschreibung Deines Problems so präzise wie möglich sein: Eine Beschreibung im Stil von "Mein Skript funktioniert nicht! Woran kann das liegen??" ist nicht sehr hilfreich, da Du sehr wenige Angaben machst.

    Versuche bei der exakten Beschreibung Deines Problems möglichst genaue Angaben über die verwendete PHP-Version, das verwendete Betriebssystem und über Software zu machen, die zusammen mit PHP eingesetzt wird (z.B. Webserver, Datenbanksystem).

  • Wenn Du Deinen Code in ein Posting einbaust, übernehme bitte nur die relevanten Zeilen, in denen Du den Fehler vermutest. Unnötige Skriptzeilen erhöhen die Größe Deines Postings und verärgern andere User, die diese unnötige Bandbreite bei Ihren manchmal sehr langsamen Verbindungen zum Usenet sehr eindrucksvoll zu spüren bekommen.

    Darüber hinaus haben viele Leute keine Lust, wegen eines kleinen Problems gleich ein Skript von mehreren Hundert Zeilen für Dich zu "debuggen".

  • Bitte übernehme Deinen Code, wenn Du ihn in Dein Posting integrierst, so, wie er auch im PHP-Skript steht: Alle Betriebssysteme unterstützen die Funktion "Copy & Paste". Nutze sie und komme bitte nicht auf die Idee, Deinen Code vom Skript in das Posting abzutippen!

    Durch das manuelle Übertragen erhöht sich die Chance, dass Du weitere Fehler in Dein ohnehin schon fehlerhaftes Skript einbaust. Damit machst Du es anderen Benutzer der Newsgroup nicht gerade leicht, die "wirklichen Fehler" zu finden.

1.18. Warum kann ich nicht mehr auf de.comp.lang.php zugreifen?

Keywords: de.comp.lang.php

Antwort von Clemens Koppensteiner

Mitte November 2002 wurde die Newsgroup de.comp.lang.php gelöscht und dafür die vier neuen Gruppen de.comp.lang.php.datenbanken, de.comp.lang.php.installation de.comp.lang.php.netzprotokolle und de.comp.lang.php.misc eingerichtet. Bitte verwende jetzt diese Gruppen.

Die Chartas der neuen Gruppen findest Du unter Wie ist die Charta dieser Newsgroup?

2. Allgemeine Fragen rund um PHP

2.1. Was ist PHP?

Antwort von Kristian Köhntopp

Die Abkürzung PHP steht offiziell für "PHP: Hypertext Preprocessor". Dies ist eine rekursive Abkürzung im Stile des GNU -Projektes. PHP ist eine serverseitige Scriptsprache zur dynamischen Erstellung von Webseiten. Die Anweisungen der Sprache sind dabei in den HTML-Code einer Webseite eingebettet, d. h. jede HTML-Seite ist auch ein gültiges PHP-Programm. Die Syntax von PHP ist ähnlich wie die von C/C++, Java oder JavaScript. Die Sprache zeichnet sich vor allen Dingen durch ihre leichte Erlernbarkeit, ihre ausgezeichneten Datenbankanbindungen und Internet-Protokolleinbindungen und die Unterstützung zahlreicher weiterer Funktionsbibliotheken aus. PHP stellt so für den Web-Entwickler das ideale Werkzeug zur Erstellung von dynamischen Inhalten dar.

PHP ist freie Software im Sinne der Debian Free Software Guidelines (DFSG). Quelltext und Binaries des PHP-Interpreters sind frei erhältlich und können für alle kommerziellen und nichtkommerziellen Zwecke eingesetzt werden; jeder kann den PHP-Quelltext weiterentwickeln und die Änderungen an das PHP-Projekt zurückfließen lassen. Der genaue Lizenztext ist in der Datei LICENSE enthalten, die Bestandteil der PHP-Distribution ist.

PHP läuft auf allen gängigen UNIX-Versionen, auf den verschiedenen Windows-Versionen (Windows 95/98/ME/NT/2000/XP) sowie auf MacOS X, OS/2 und einigen anderen Betriebssystemen. Als CGI-Programm kann PHP mit jedem Webserver zusammenarbeiten. Für einige Webserver, allen voran Apache, stehen auch Modulversionen zur Verfügung, die sehr viel effizienter ausgeführt werden. Mit PHP 4.3.0 gibt es auch ein Command Line Interface (CLI), welches PHP für kommandozeilenbasierende Applikationen attraktiv macht.

Die Homepage des PHP-Projektes ist http://www.php.net. Mirrors dieser Site sind in vielen Ländern vorhanden, unter anderem auch in Deutschland unter der URL http://www.php3.de oder http://de.php.net. Von dort kann man die jeweils aktuelle Releaseversion des Interpreters sowie Binaries für eine Reihe von Plattformen herunterladen. Ebenso finden sich dort das Handbuch und Archive der englischen Mailinglisten. Die Geschichte von PHP ist ebenfalls dokumentiert.

2.2. Welche Version von PHP ist aktuell?

Keywords: Version | aktuell

Antwort von Kristian Köhntopp

Die aktuelle Produktionsversion von PHP ist Version 5.0.3. Diese Version ist die erste ausreichend stabile Version seit dem Release von PHP 5.0.0 am 13.07.2004. Wer auf Stabilität wert legt, sollte jedoch derzeit noch PHP 4 verwenden.

Die derzeit letzte Version von PHP 4 ist Version 4.3.10. An PHP 4 werden derzeit noch Bugfixes vorgenommen und kleinere Änderungen aus der aktuellen Version zurückübernommen (auch "MFH" oder Merge from Head" genannt).

Die letzte Version von PHP 3 war Version 3.0.18 (bzw. 3.0.17 für die kompilierte Windows-Version). PHP 3 wird nicht mehr weiterentwickelt und soll nicht mehr verwendet werden.

Die aktuellste Version ist immer auf der offiziellen PHP Homepage verfügbar.

2.3. Wo bekomme ich PHP?

Keywords: PHP4 | Version | Download | Source | Mirror

Antwort von Kristian Köhntopp

Wähle Download auf der PHP Website oder einem der Mirrors.

2.4. Was ist neu in PHP5.1?

Antwort von Kristian Köhntopp

PHP 5.1 ist ein inkrementelles Update auf der Basis der Zend II Engine und des Objektmodells von PHP 5. Die Änderungen sind jedoch - auch wenn sie nur Module betreffen - signifikant.

PDO (PHP Data Objects)

PDO stellt eine einheitliche Datenbankabstraktion zur Verfügung, die Bestandteil der Sprache selbst ist und keine externe Bibltiotek.

PCRE 5.0

Update der PCRE-Bibliothek für reguläre Ausdrücke auf Version 5.0

SPL (Standard PHP Library) Update

Die SPL stellt eine Reihe von Einbaufunktionen und Klassen zur Verfügung, die die folgenden Bereiche abdecken: Iteratoren, Datei- und Verzeichniszugriff, XML-Zugriff, Array-Overloading, auf- und abzählbare Datenstrukturen, Umgang mit Exceptions, Standard-Design-Patterns

WSDL

Verbesserter Support für SOAP und Webservices durch WSDL-Unterstützung

2.5. Was ist neu in PHP5?

Antwort von Kristian Köhntopp

PHP5 besteht aus dem Sprachkern Zend und den Funktionsmodulen, die den eigentlichen Wert von PHP ausmachen.

Änderungen am Sprachkern:

Umstellung auf die Zend II Engine

Die neue Engine liefert ein neues Objektmodell, das PHP zu einer Sprache mit einem schnellen und erstklassigen Objektsystem macht.

Änderungen an den Bibliotheken und Modulen:

Verbesserter XML Support

PHP 5 verwendet libxml2 und erlaubt es, mit dieser Bibliothek über SAX, DOM oder die Simple XML Extension auf XML-Daten zuzugreifen sowie XSLT-Transformationen durchzuführen.

SOAP Support

Mit Hilfe der SOAP-Extension können Webservices einfach und sicher aufgerufen werden.

MySQLi Support

Die neue MySQLi-Extension erlaubt es, auf neuere Versionen von MySQL zuzugreifen. Dabei kann wahlweise ein traditioneller prozeduraler oder ein objektorientierter Ansatz verwendet werden. Die neue MySQL-API erlaubt es, Variablen zu binden. Wird dies gemacht, werden SQL-Injections sehr viel schwieriger.

SQLite Integration

Durch Integration der dateibasierten Mini-SQL-Implementierung SQLite ist es möglich, Anwendungen mit Flat-File-Zugriff genauso wie datenbankbasierende Anwendungen zu schreiben.

Streams

Der Support für I/O Streams ist stark vebessert und es bestehen nun viel mehr Optionen zur Datenbehandlung bei der Ein- und Ausgabe.

2.6. Was ist neu in PHP4?

Antwort von Kristian Köhntopp

PHP4 besteht aus dem Sprachkern Zend und den Funktionsmodulen, die den eigentlichen Wert von PHP ausmachen.

Änderungen am Sprachkern:

PHP4 ist ein Interpiler

PHP3 war eine interpretierte Sprache. Code wurde nur minimal gecached. Das hatte zur Folge, dass Code in Schleifen oder in oft aufgerufenen Funktionen wieder und wieder neu geparsed werden musste, bevor er ausgeführt werden konnte.

PHP4/Zend ist ein Bytecode-Compiler, der beim Programmstart aufgerufen wird und das komplette Programm in eine interne Darstellung einer virtuellen Maschine überführt. Danach beginnt die Interpretation des Bytecodes der VM. Dies ist dasselbe Funktionsprinzip wie bei Perl, und ganz ähnlich dem Funktionsprinzip von Java, nur dass dort der Compiler explizit aufgerufen werden muss.

PHP4/Zend ist bei einigen Sprachkonstrukten zehnmal schneller als PHP3. Es liegt in den meisten Anwendungen gleichauf mit Perl oder mit Microsoft Visual Basic.

PHP4 ist threadsafe

PHP4 kann endlich in Multithreaded-Umgebungen eingesetzt werden. Dadurch wird es möglich, PHP4 als Modul im Roxen Webserver, im Internet Information Server, im AOL/Netscape-Server und in einigen anderen Webservern einzusetzen.

PHP4 kann außerdem als CGI-Programm und als Coprozess zum Webserver eingesetzt werden, der als Java Servlet Engine behandelt wird.

Referenzen sind endlich Teil der Sprache

In PHP3 waren Referenzen nur beim Aufruf von Funktionen verwendbar, um Call-by-Reference zu realisieren. In PHP4/Zend gibt es nun endlich vollständigen Support für Referenzen. Das bedeutet:

$foo = &$a;

In diesem Beispiel wird $foo zu einem alternativen Namen für die Variable $a. Dies funktioniert auch mit Arrays und Objekten.

Referenzzähler und Ressourcenfreigabe

In PHP3 existierten keine Zähler für Datenobjekte außerhalb der Sprache, etwa Image-Handles oder Datenbank-Resultathandles. Diese Ressourcen mussten manuell freigegeben werden.

PHP4/Zend hat für alle Variablen vom Typ Ressource Referenzzähler. Objekte, die nicht mehr referenziert werden, werden automatisch freigegeben. Das bedeutet: Aufrufe zu Funktionen wie mysql_free_result() innerhalb von Schleifen sind nicht mehr zwingend notwendig.

Demnach tut unset() jetzt auch endlich, was es soll.

Session-Support

PHP4 enthält Sessions nach dem Vorbild von PHPLIB als eingebauter Bestandteil der Sprache. Sessions werden standardmäßig als Dateien in einem Verzeichnis abgelegt, aber über Sessionmodule kann auch eine Datenbank, ein Shared Memory Segment oder ein anderer persistenter Speicher verwendet werden.

Anders als PHPLIB kann PHP4 die Session-ID automatisch an URLs anfügen.

Output Buffering

Mit Hilfe der Funktion ob_start() kann eine Ausgabepufferung aktiviert werden. Der Ausgabepuffer wird erst am Ende der PHP-Seite oder mit Hilfe der Funktion ob_end_flush() ausgegeben. Die Funktion ob_end_clean() verwirft den Ausgabepuffer. ob_get_contents() überträgt den Ausgabepuffer in eine Variable.

Headerinformation wie sie von header() und setcookie() erzeugt wird ist ungepuffert. Indem man Ausgabepufferung aktiviert, kann man überall in der Datei Headerinformationen senden, egal ob vorher schon Ausgabe erzeugt wurde oder nicht. Ausgabepufferung erhöht die Latenzen bei der Ausgabe der Webseite je nach Anwendung erheblich und kann PHP sehr viel langsamer erscheinen lassen.

Verbesserter Objektsupport

In PHP4/Zend ist es möglich, die Objektnotation von PHP zu verwenden, um objektorientiert aufgebaute Funktionsbibliotheken zu verwenden. Zum Beispiel verwenden die COM-Erweiterung und die DOMXML-Erweiterung von PHP4 diese Funktionalität.

Außerdem enthält PHP4 nun endlich Funktionen, mit denen Objekte über sich selbst Auskunft geben können: get_class() , get_parent_class() , method_exists() , is_subclass_of()

include() und eval() sind Funktionen

In PHP4 sind die früheren Anweisungen include() und eval() jetzt Funktionen, die einen Wert zurückgeben können. Der Default-Returnwert ist 1, damit Anweisungen wie if (include(...)) funktionieren.

Mit Hilfe der Anweisung return() kann man den Returncode eines include() oder eval() -Aufrufes setzen.

Verbesserter Parser

Der PHP3-Parser hatte Probleme mit Verschachtelungen von Objekten in Arrays. In PHP4/Zend ist dieses Problem behoben.

Shell-Style Here-Documents werden unterstützt.

Es gibt ein foreach() -Schleifenkonstrukt. Syntax:

foreach($my_array as $val)
    print "$val\n";

foreach($my_array as $key => $val)
    print "$key => $val\n";
Boolean

Die Schlüsselworte true und false sind nun Teil der Sprache und es gibt einen Datentyp boolean. Vergleiche werden nun durchgeführt, indem ein fremder Datentyp in Boolean konvertiert wird und dann verglichen wird: In PHP4/Zend ist das Konstrukt 5 == true eine wahre Aussage, weil (boolean) 5 in true konvertiert wird, bevor verglichen wird.

Laufzeitbindung von Funktionsnamen

Funktionen können nun aufgerufen werden, bevor sie deklariert werden.

Namespace aufgeräumt, Konfiguration überarbeitet

Der Import von Variablen in den globalen Namespace war bisher nur schwierig mit der Konfigurationsdirektive gpc_order steuerbar: Zwar konnte man die Reihenfolge unterdrücken, aber es war nicht möglich, den Import von externen Werten in globale Variablen zu verhindern, ohne diese Werte auch in den Track Vars auszuschließen. Umgebungsvariablen und Sessionvariablen waren gar nicht kontrollierbar.

In PHP4 kann man nun den Import von Werten differenziert steuern, und zwar unabhängig voneinander als globale Variablen und als Track Vars.

Ebenso wurde der Konfigurationsmechanismus überarbeitet: Es ist nun möglich, PHP mit denselben Direktiven in der Apache-Konfiguration und in der php.ini zu steuern.

Neue Funktionen und Module

Die Sprache ist außerdem um zahlreiche Funktionen und neue Module erweitert worden.

2.7. Wo finde ich weitere Informationen über PHP?

Antwort von Kristian Köhntopp

Zu PHP gibt es zahlreiche Informationsquellen in deutscher und englischer Sprache.

Deutsche Ressourcen im WWW

Internationale Ressourcen im WWW

Schulungsunterlagen für PHP

Fertige Anwendungen in PHP

Ein Verzeichnis von Projekten, die PHP verwenden, findet man im Projektverzeichnis der offiziellen Homepage.

  • Phorum, ein Diskussionsforum.

  • phpSlash, ein Diskussionsforum.

  • IMP, ein Webmail Interface.

  • Bookmarker, eine Bookmark-Verwaltung.

  • PHPLIB, eine objektorientierte Bibliothek zur Anwendungsentwicklung.

  • phpMyAdmin, ein Managementsystem für MySQL-Datenbanken.

  • MyGuestbook, ein Gästebuch.

  • phpAds, ein Verwaltungssystem für Banner Ads.

  • phpHoo, eine Art Mini-Yahoo.

2.8. Welche Editoren sind für PHP geeignet?

Antwort von Matthias P. Wuerfl

Prinzipiell ist selbstverständlich jeder ASCII-Editor für PHP geeignet. Syntax-Highlighting und automatische Vervollständigung von Funktionsnamen bietet schon der größte Teil der Editoren. Einige IDEs bieten auch

  • die Integration des PHP-Manuals

  • die Möglichkeit des Debuggings (Stop-Punkte, Einzelschritte, ...)

  • die Integration in Versionsverwaltungssysteme (z.B. CVS)

Unter PHP Editor Rewiew ist eine umfangreiche Liste einsehbar, in der verschiedene Editoren/IDEs mit Bewertungen und Kommentaren aufgeführt sind.

2.9. Gibt es für PHP einen "Dokumentationsgenerator" ähnlich Javadoc für Java?

Antwort von Guido Haeger

PHPDocumentor ist allerdings das einzige dieser Projekte, welches in letzter Zeit ständig weiterentwickelt wurde und als "stable" gekennzeichnet ist.

Antwort von Richard Körber

Einen PHPDoc-Generator, der auf Java basiert und sogar die JavaDoc-Doclets verwendet (zum Beispiel zur Ausgabe in PDFs), gibt es hier: PHPDoc (von Christian Calloway).

Antwort von Sebastian Bergmann

Doxygen unterstützt seit Version 1.2.17 neben C, C++, C#, Java und IDL auch PHP.

2.10. Was bedeutet LAMP, WAMP und so weiter?

Antwort von Kristian Köhntopp

LAMP ist die Abkürzung für Linux, Apache, MySQL und PHP. Sie beschreibt ein System zur Entwicklung und zum Betrieb von Webanwendungen, bestehend aus Betriebssystem, Webserver, Datenbankserver und Programmiersprache.

Analog steht die Abkürzung WAMP für Windows, die Windows-Version von Apache, die Windows-Version von MySQL und die Windows-Version von PHP. Viele PHP-Anwender entwickeln lokal auf WAMP und überspielen die fertigen Seiten dann auf einen LAMP- oder SAMP (Solaris, Apache, MySQL, PHP)-Server bei einem Provider.

2.11. Gibt es Application Server für PHP?

Keywords: Application | Server | SRM | Pooling | Daemon | Session

Antwort von Sebastian Bergmann

Der Begriff "Application Server" ist ein sehr schwammiger.

Man könnte unter einem solchen beispielsweise einen Daemon (Server) verstehen, der die Ausgabe von Programmen (Application) über das Internet zur Verfügung stellt. Dieser Definition zufolge wäre bereits ein Webserver mit PHP Unterstützung ein "Application Server".

In der Java Welt hingegen versteht man jedoch etwas anderes unter dem Begriff des "Application Servers":

In einem "Application Server" laufen die Applikationen permanent ab. Kommt ein Request per HTTP, so wird eine entsprechende Methode einer bereits laufenden Anwendung aufgerufen. Diese liefert ein Ergebnis, das an den Client ausgeliefert wird.

Dies bringt mehrere Vorteile mit sich: Die Umgebung der Sprache, in der die Applikation geschrieben wurde -- im Falle von PHP der PHP Interpreter, im Falle von Java die Java Virtual Machine (JVM) (naja, eigentlich ist auch der PHP Interpreter eine virtuelle Maschine, und die Java Virtual Machine ein Bytecode Interpreter -- aber das soll jetzt hier nicht diskutiert werden) -- muss nicht für jeden einzelnen HTTP Request neu initialisiert werden. Ferner können Ressourcen, wie zum Beispiel Datenbankverbindungen, gemeinsam genutzt und somit effizienter verwaltet werden.

Noch schwerer fällt als Vorteil ins Gewicht, dass komplexe Datenstrukturen nur einmal zu Beginn der Applikation aufgebaut werden müssen -- man denke hierbei nur an die OOHForms von Ulf Wendel.

Auch erschließen sich dem Applikationsentwickler neue Möglichkeiten, da er mit einem Application Server Daten zwischen verschiedenen Requests an seine Applikation tauschen kann: denn eigentlich arbeitet ja nur eine Instanz der Applikation sämtliche Anfragen ab.

Zur Zeit gibt es für PHP noch keinen Application Server, der dem Begriff aus der Java Welt gerecht wird. Unter dem Namen Script Running Machine (SRM) arbeiten einige PHP Developer an einem Daemon, der PHP Applikationen permanent ausführen, Datenbankverbindung poolen und Datenstrukturen im Speicher halten kann.

2.12. Was passiert, wenn ich eine PHP-Seite aufrufe?

Keywords: Server | Client | Browser | Internet | HTTP | .php

Antwort von Clemens Koppensteiner

Ohne genauer in die Details zu gehen (immerhin ist das hier eine FAQ zu PHP und nicht zur Funktionsweise des Internets), kann man den Vorgang zwischen dem Eintippen der URL und der Anzeige der (HTML) Daten etwa folgendermaßen zusammenfassen:

Der Client (also der Web-Browser) schickt eine Anfrage an den Server auf dem die PHP-Seite gespeichert ist mit einer Anfrage nach, z.B., index.php. Die Sprache (das Protokoll), in der diese Anfrage gesendet wird, nennt sich HTTP (Hypertext Transfer Protocol, daher beginnen die URLs von Webseiten auch mit "http://").

Am Server wird diese Anfrage von einem speziellen Programm, dem HTTP-Server (z.B. Apache) erhalten. Wenn nun eine statische HTML-Seite (.html) gewünscht ist, ließt sie der Server direkt aus und schickt sie zurück an den Client. Ist aber eine PHP-Seite gefragt, leitet er die Anfrage an den PHP-Präprozessor (dem Programm das sozusagen hinter der Programmiersprache PHP steht) weiter. Dieser ließt nun den PHP-Quelltext der Seite, parst ihn und generiert daraus die HTML-Seite, die an den HTTP-Server und schlussendlich zum Client weitergeleitet wird.

Der Browser stellt nun das zurückgegeben HTML (hier ist also kein PHP-Code mehr enthalten) dar und führt eventuell enthaltenen JavaScript Code aus.

2.13. Wer ist der komische Typ mit den Bleistiften im Mund?

Antwort von Kristian Köhntopp

Ruft man phpinfo() auf, bekommt man normalerweise das PHP Logo oben in der rechten Ecke angezeigt. Dies wird realisiert durch einen Verweis auf die URL der phpinfo-Seite selbst mit einer speziellen Objekt-ID als Parameter. Am ersten April erscheint dort ein Verweis auf die spezielle Objekt-ID =PHPE9568F36-D428-11d2-A769-00AA001ACF42, die ein Bild von Thies C. Arntzen referenziert, einem Mitglied des PHP Core Teams und Autor zahlreicher Extensions.

3. Installation und Inbetriebnahme

3.1. Wie kompiliere ich ein aktuelles PHP auf Linux mit Apache Server?

Antwort von Kristian Köhntopp

Eine gute Hilfe findet sich bei Apache 2 and PHP (mod_php) on Linux.

3.2. Ich habe Probleme PHP selbst zu kompilieren.

Antwort von Kristian Köhntopp

config.cache nicht gelöscht.

Beim Selbstübersetzen von PHP werden gelegentlich neu installierte Bibliotheken nicht oder in der falschen Version gefunden. In diesem Fall ist meistens eine veraltete Datei config.cache aus einem früheren Lauf von configure schuld. Diese Datei muss gelöscht werden, danach werden die neuen Bibliotheken korrekt gefunden.

3.3. Wie installiere ich CGI-PHP auf einem Apache-Server?

Keywords: Installation | Apache | Server | CGI

Antwort von Kristian Köhntopp

Schritt 1: Man definiert ein Verzeichnis als CGI Verzeichnis:

<Directory /home/www/servers/phplib.shonline.de/cgi/>
Options ExecCGI
AllowOverride None
</Directory>

Hier wird das Verzeichnis /home/www/.../cgi als Verzeichnis zur Ausführung von CGI-Dateien gekennzeichnet, indem für das Verzeichnis das ExecCGI Attribut gesetzt wird.

Schritt 2: Man definiert für dieses Verzeichnis eine URL

ScriptAlias  /cgi/      /home/www/servers/phplib.shonline.de/cgi/

Die URL /cgi/ wird jetzt auf dieses Verzeichnis /home/www/.../cgi/ abgebildet.

Man kann jetzt in diesem Verzeichnis eine ausführbare Datei (etwa ein Shellscript) ablegen, die den folgenden Text ausgibt, wenn man sie startet.

Content-Type: text/plain

Hallo, Welt.

Wenn man diese Datei unter der URL http://meinserver.de/cgi/name_des-scripts aufruft, muss man den Text "Hallo, Welt." angezeigt bekommen. Gelingt dies nicht, sollte man durch das Error Log toben und nachsehen, was dort schiefgeht.

Schritt 3: Man installiert den PHP-Interpreter im CGI-Verzeichnis und prüft, ob man ihn als CGI-Programm aufrufen kann. Dazu ist der Interpreter nach /home/www/.../cgi zu kopieren und dann unter der URL http://.../cgi/php oder http://.../cgi/php.exe oder wie immer der Interpreter heißt aufzurufen.

Dabei muss die folgende Fehlermeldung kommen:

Security Alert! PHP CGI cannot be accessed directly.

This PHP CGI binary was compiled with force-cgi-redirect
enabled. This means that a page will only be served up if the
REDIRECT_STATUS CGI variable is set. This variable is set, for
example, by Apache's Action directive redirect.

Kommt die Meldung nicht, ist das PHP-Binary unsicher und sollte durch ein korrekt kompiliertes Binary ersetzt werden.

Schritt 4: Man definiert eine Scriptaktion, die das o.a. Binary startet und definiert eine Endung, die durch diese Aktion abgehandelt wird:

  • Action       php-script /cgi/php
    

    teilt dem Apache mit, dass der interne MIME-Typ php-script durch die URL /cgi/php (oder wie immer die URL zum Aufruf des PHP-Interpreters aus Schritt 3 heißt) abgehandelt wird. Der MIME-Typ php-script ist illegal und das ist gut so, denn er wird nur innerhalb von Apache verwendet (er darf nur innerhalb von Apache verwendet werden!).

  • AddHandler   php-script .php
    

    teilt Apache weiterhin mit, dass die Endung .php durch den MIME-Handler für den MIME-Type php-script abgehandelt wird.

3.4. Wie installiere ich PHP auf Windows?

Antwort von Martin Jansen

Bei der Installation von PHP unter Windows hat man ebenso wie unter Unix/Linux die Möglichkeit, PHP als CGI-Version oder als Modul für den Apache Webserver zu installieren. Die Vor- und Nachteile der verschiedenen Versionen werden im Artikel CGI PHP oder Modul? erläutert.

Sowohl die CGI- als auch die Modul-Version sind auf der offiziellen Downloadseite erhältlich.

Mit der Modul-Version ist es zur Zeit jedoch nicht möglich, die GD-Library bzw. die Sablotron-Library zu nutzen, da diese nicht threadsafe sind.

Die offizielle Installationsanleitung für die CGI-Version in englischer Sprache befindet sich auf dem offiziellen PHP-Server.

In deutscher Sprache findet man zur selben Frage eine umfangreiche WAMP-Anleitung auf infos24. Viele weitere Tutorials zu diesem Thema findet Google.

Zur Installation der Modul-Version schrieb Björn Höhrmann am 20.07.2000 in de.comp.lang.php folgendes:

Zur Installation:

WinNT4:       E:\Winnt\
Apache:       E:\winapp\apache\
PHP4 Module:  E:\winapp\apache\php4module\

E:\winapp\apache\conf\httpd.conf:
 + LoadModule php4_module php4module/php4apache.dll
 + AddType application/x-httpd-php .php

E:\winapp\apache\php4module\php.ini:
 > Kopiert nach E:\Winnt\

[...]

E:\winapp\apache>set path=%path%;e:\winapp\apache\php4module\
E:\winapp\apache>apache -k start
Apache/1.3.11 (WunLix) PHP/4.0.2-dev running...

[...]

Darüber hinaus gibt es mehrere Pakete, die eine komplette Serverumgebung inkl. PHP und MySQL auf einem Windows-Rechner installieren.

  • AppServ Open Project

  • phpdev (nicht ganz aktuell)

  • EasyPHP (relativ aktuell)

  • Das derzeit aktuellste Paket ist XAMPP für Windows, das auch mod_perl, mod_ssl und einiges mehr enthält. Es ist auch eine Version mit PHP 5 erhältlich, wenn man seine Programme bereits mit PHP 5 testen will.

3.5. Linux: Meine shared libraries werden nicht gefunden.

Antwort von Kristian Köhntopp

Übersetzt man PHP selbst und bindet dabei auch selbst installierte Module und Bibliotheken mit ein, kann es vorkommen, dass das Modul vom Apache nicht geladen wird, weil diese externen Bibliotheken nicht gefunden werden. Im folgenden wird erläutert, was es mit diesen Bibliotheken auf sich hat.

kris@valiant:~ > ldd /bin/ls
        libc.so.6 => /lib/libc.so.6 (0x4001f000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Damit man das Kommando ls ausführen kann, müssen die Dateien ld-linux.so.2 und libc.so.6 vorhanden sein. Diese Dateien sucht mein System in den in /etc/ld.so.conf genannten Verzeichnissen. Damit dies schneller geht, legt das Kommando ldconfig einen Cache der Bibliotheken in diesen Verzeichnissen an. Mit diesen Caches wird ein Durchsuchen aller Verzeichnisse in /etc/ld.so.conf beim Start von Programmen vermieden. Stattdessen stehen in /etc/ld.so.cache Paare von Bibliotheksname und Pfadname, die sofort zugreifbar sind.

Ein laufendes ls hat also /lib/ld-linux.so.2 und /lib/libc.so.6 geladen. Das kann man auch nachsehen:

kris@valiant:~ > sleep 3600 &
[1] 27091
kris@valiant:~ > cat /proc/27091/maps
08048000-0804a000 r-xp 00000000 08:15 63535      /bin/sleep
0804a000-0804b000 rw-p 00001000 08:15 63535      /bin/sleep
40000000-40013000 r-xp 00000000 08:15 71682      /lib/ld-2.1.3.so
40013000-40014000 rw-p 00012000 08:15 71682      /lib/ld-2.1.3.so
4001e000-4001f000 rw-p 00000000 00:00 0
4001f000-400f9000 r-xp 00000000 08:15 71690      /lib/libc.so.6
400f9000-400fe000 rw-p 000d9000 08:15 71690      /lib/libc.so.6
400fe000-40101000 rw-p 00000000 00:00 0
bfffe000-c0000000 rwxp fffff000 00:00 0
kris@valiant:~ > ldd `which sleep`
        libc.so.6 => /lib/libc.so.6 (0x4001f000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
kris@valiant:~ > ls -l /lib/ld-linux.so.2
lrwxrwxrwx   1 root     root       11 Apr 11 18:16 /lib/ld-linux.so.2
                                                   -> ld-2.1.3.so
kris@valiant:~ > kill %1
kris@valiant:~ >
[1]+  Terminated              sleep 3600

Tatsächlich gibt die Ausgabe der Memory-Map für den Prozess 27091 (sleep) an, dass /lib/ld-2.1.3.so aka /lib/ld-linux.so.2 und /lib/libc.so.6 geladen sind - man kann deutlich die schreibgeschützten Code-Segmente (r-xp) und die überschreibbaren, nicht ausführbaren Datensegmente (rw-p) dieser Bibliotheken erkennen.

Will ich nun mein PHP4 starten, dann wird

kris@valiant:~ > ldd /usr/lib/apache/libphp4.so
        libgdbm.so.2 => /usr/lib/libgdbm.so.2 (0x4015b000)
        libpam.so.0 => /lib/libpam.so.0 (0x40162000)
        librecode.so.0 => /usr/lib/librecode.so.0 (0x4016a000)
        libdl.so.2 => /lib/libdl.so.2 (0x401e6000)
        libsnmp.so => /usr/lib/libsnmp.so (0x401ea000)
        libpdf.so.0 => /usr/lib/libpdf.so.0 (0x40225000)
        libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4024e000)
        libldap.so.1 => /usr/lib/libldap.so.1 (0x40291000)
        libttf.so.2 => /usr/lib/libttf.so.2 (0x402a6000)
        libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x402cf000)
        libpng.so.2 => /usr/lib/libpng.so.2 (0x402ee000)
        libz.so.1 => /usr/lib/libz.so.1 (0x4030b000)
        libresolv.so.2 => /lib/libresolv.so.2 (0x4031b000)
        libm.so.6 => /lib/libm.so.6 (0x4032a000)
        libcrypt.so.1 => /lib/libcrypt.so.1 (0x40347000)
        libnsl.so.1 => /lib/libnsl.so.1 (0x40374000)
        liblber.so.1 => /usr/lib/liblber.so.1 (0x4038b000)
        libc.so.6 => /lib/libc.so.6 (0x40390000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

geladen, d.h. ein

kris@valiant:~ > grep LoadModule /etc/httpd/httpd.conf | grep php
LoadModule php4_module /usr/lib/apache/libphp4.so

in der Apache-Konfiguration kann nur dann gelingen, wenn all die o.a. Bibliotheken vorhanden sind. Sind sie es nicht, schlägt das Laden des Moduls fehl:

kris@valiant:~ > su -
Password:
valiant:~ # mv /usr/lib/libpng.so.2 /usr/lib/libpng.so.2.offline
valiant:~ # ldd /usr/lib/apache/libphp4.so
        libgdbm.so.2 => /usr/lib/libgdbm.so.2 (0x4015b000)
        libpam.so.0 => /lib/libpam.so.0 (0x40162000)
        librecode.so.0 => /usr/lib/librecode.so.0 (0x4016a000)
        libdl.so.2 => /lib/libdl.so.2 (0x401e6000)
        libsnmp.so => /usr/lib/libsnmp.so (0x401ea000)
        libpdf.so.0 => /usr/lib/libpdf.so.0 (0x40225000)
        libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4024e000)
        libldap.so.1 => /usr/lib/libldap.so.1 (0x40291000)
        libttf.so.2 => /usr/lib/libttf.so.2 (0x402a6000)
        libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x402cf000)

<<<     libpng.so.2 => not found >>>

        libz.so.1 => /usr/lib/libz.so.1 (0x402ee000)
        libresolv.so.2 => /lib/libresolv.so.2 (0x402fe000)
        libm.so.6 => /lib/libm.so.6 (0x4030d000)
        libcrypt.so.1 => /lib/libcrypt.so.1 (0x4032a000)
        libnsl.so.1 => /lib/libnsl.so.1 (0x40357000)
        liblber.so.1 => /usr/lib/liblber.so.1 (0x4036e000)
        libc.so.6 => /lib/libc.so.6 (0x40373000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
valiant:~ # mv /usr/lib/libpng.so.2.offline /usr/lib/libpng.so.2
valiant:~ # logout
kris@valiant:~ >

Note: Do not try this at home, kids! Wenn man das mit der falschen Bibliothek macht (ld-linux und libc sind gute Kandidaten), kann man schon mal die Rettungs-CD rauskramen und die Termine am Nachmittag absagen.

Wenn die fehlende Bibliothek also nicht installiert ist, kann es nicht gehen. Wenn die fehlende Bibliothek nicht gefunden wird, dann ist /etc/ld.so.conf unvollständig und muss ergänzt werden. Wenn die fehlende Bibliothek nicht gefunden wird, aber installiert ist und in einem in /etc/ld.so.conf genannten Pfad installiert ist, dann muss ldconfig neu aufgerufen werden, damit /etc/ld.so.cache neu gebaut wird.

3.6. SuSE Linux: Beim compilieren wird lex nicht gefunden

Antwort von Matthias P. Wuerfl

Um PHP auf den aktuellen SuSE-Distributionen zu installieren braucht man flex. Wenn flex nicht installiert ist, dann bricht ./configure mit Meldungen ab wie:

checking for flex... no
checking for lex... no
./configure: flex: command not found
checking for flex... lex
checking for yywrap in -ll... no
checking lex output file root... ./configure: lex: command not found
configure: error: cannot find output from lex; giving up

Flex findet man in der Serie d auf den SuSE-CDs und auch auf dem SuSE ftp-Server. Es kann einfach per YaST installiert werden.

4. Konfiguration

4.1. Wie finde ich heraus, wie mein PHP-Interpreter konfiguriert ist?

Antwort von Kristian Köhntopp

Die PHP-Funktion phpinfo() liefert eine Fülle von Information über die aktuelle Konfiguration des Interpreters. Das typische Testprogramm sieht so aus:

<?php
  phpinfo();
 ?>

4.2. Wo finde ich die php.ini?

Antwort von Johannes Frömter

Die Ausgabe von phpinfo() sagt unter anderem, wo PHP seine Konfigurationsdatei php.ini sucht. Steht hier nur php.ini, hat PHP keine Datei gefunden und arbeitet mit Default-Einstellungen (diese entsprechen etwa denen aus der php.ini-dist); weniger irreführend ist die Ausgabe von echo get_cfg_var('cfg_file_path'), sie ist in diesem Fall einfach leer.

Bei der Kompilierung von PHP kann mit der Direktive --with-config-file-path=/dir ein Verzeichnis definiert werden; default-mäßig erwartet PHP die php.ini unter Unix in /usr/local/lib und unter Windows im Windows-Verzeichnis. Dorthin kopiert man die mitgelieferte php.ini-dist und benennt sie in php.ini um.

Ab der Version 4.3.0 kann bei der Kompilierung von PHP mit der Direktive --with-config-file-scan-dir=PATH ein zusätzliches Verzeichnis definiert werden, in dem zusätzliche php.ini Dateien liegen können. Wurde PHP mit dieser Option kompiliert und liegen php.ini Dateien in diesem Verzeichnis, haben die Einstellungen Vorrang gegenüber der vorher gefundenen php.ini.

Neben der php.ini und den .htaccess-Dateien gibt es unter Windows auch die Möglichkeit, in der Registry PHP-Konfigurations-Einstellungen vorzunehmen - ob dies der Übersichtlichkeit dienlich ist, sei dahingestellt... Die Einstellungen wirken sich als 'local value' (siehe phpinfo() ) auf bestimmte Verzeichnisse sowie deren Unterverzeichnisse aus.

  1. Regedit öffnen

  2. Den Schlüssel HKEY_LOCAL_MACHINE\SOFTWARE öffnen

  3. Den Schlüssel PHP anlegen

  4. Den Schlüssel Per Directory Values anlegen

  5. Einen Schlüssel für das Laufwerk anlegen, in dem sich das Document Root befindet, z.B. C oder D (ohne ':\'!)

  6. Einen Schlüssel für das Document-Root-Verzeichnis anlegen, z.B. 'www'

  7. (Für jedes Unterverzeichnis einen weiteren Unterschlüssel anlegen, z.B. 'php')

  8. Einen neuen "Zeichenfolgenwert" anlegen, z.B 'auto_prepend_file'

  9. Dem neuen Eintrag einen Wert zuweisen, z.B. 'prepend.php'

Der vollständige Schlüssel aus obigem Beispiel sollte nun so aussehen: HKEY_LOCAL_MACHINE\SOFTWARE\PHP\Per Directory Values\C\www\php -> auto_prepend_file = 'prepend.php'. Anmerkung: Egal ob der Eintrag ein bool'scher Wert, Integer oder String ist, der Schlüssel muss immer vom Typ "Zeichenfolgenwert" (engl. "string") sein.

4.3. Was bedeuten master value und local value in phpinfo()?

Antwort von Kristian Köhntopp

Die Unterscheidung hat nur in der Modulversion von PHP einen Sinn:

Der Master Value ist der Wert, der in der php.ini eingetragen ist. Diese Datei wird nur beim Neustart des PHP-Interpreters (beim Modul also beim Neustart des Webservers) eingelesen und beachtet. Diese Werte gelten überall auf dem Server, wenn kein besonderer local value definiert ist.

Der Local Value ist der Wert, der in diesem Verzeichnis gilt. Er kann in einem Apache <Directory>-Block oder in einer .htaccess-Datei eingetragen sein. In ersterem Fall wird er beim Neustart des Servers neu gelesen, in letzterem Fall wird er bei jedem Zugriff neu gelesen und beachtet.

4.4. Welche Konfigurationsvariablen kann ich nicht in .htaccess-Dateien verwenden?

Antwort von Kristian Köhntopp

Sicherheitsrelevante Konfigurationseinträge sind nicht über .htaccess-Dateien steuerbar, sondern nur über Einträge in <Directory> oder <Location>-Blocks in der zentralen Konfigurationsdatei. Sonst wären sie auch sinnlos, da die .htaccess-Dateien ja durch den Anwender kontrolliert werden.

Welche Werte wo definiert werden dürfen, ist im offiziellen PHP-Manual offiziellen PHP-Manual im Detail beschrieben.

4.5. Was ist --enable-force-cgi-redirect? Warum enthält $_SERVER['PHP_SELF'] den Pfad zum CGI-Interpreter?

Antwort von Kristian Köhntopp

Wenn CGI-PHP eingesetzt wird, bekommt der Interpreter den Pfad zum PHP-Script in der CGI-Variablen $_SERVER['PATH_INFO'] übergeben. Der Aufruf eines PHP-Scriptes erfolgt also mit Hilfe einer URL der Form

http://www.example.com/cgi-bin/php/somedir/somescript.php

Der PHP-Interpreter wird dabei unter der URL http://www.example.com/cgi-bin/php erreicht und die Variable $_SERVER['PATH_INFO'] erhält den Wert /somedir/somescript.php. Der PHP-Interpreter greift sich den Wert der Konfigurationsvariablen document_root aus der php.ini und setzt den Dateinamen des Scriptes aus dieser Variablen und $_SERVER['PATH_INFO'] zusammen.

Dies ist natürlich gefährlich, denn ein böswillger Anwender könnte jetzt anfangen, Scripte wie folgt aufzurufen:

http://www.example.com/cgi-bin/php/../../../../../etc/passwd

Der PHP-Interpreter würde nun anfangen, die passwd-Datei als Script zu laden und auszuführen, d.h. die Passwort-Datei auszugeben. Auf diese Weise kann man prinzipiell auf jede Datei im System zugreifen, sofern diese durch den Benutzer lesbar ist, unter dessen UID der Interpreter abläuft.

Übersetzt man CGI-PHP mit der Konfigurationsoption enable-force-cgi-redirect, funktioniert dies nicht mehr. PHP startet in diesem Fall nur noch, wenn die CGI-Umgebungsvariable $_SERVER['REDIRECT_STATUS'] gesetzt ist (der Wert der Variablen ist egal). Der Apache Server setzt diese Variable, wenn der Zugriff auf das CGI-Verzeichnis die Folge eines eines serverinternen Redirect ist. Üblicherweise konfiguriert man seinen Webserver dann so:

  Action     php-script /cgi-bin/php
  AddHandler php-script .php

Die Direktive AddHandler bildet die Endung .php auf den Handler php-script (der Name ist frei wählbar, solange er eindeutig ist) ab. Die Direktive Action deklariert eine externe Aktion, die durch ein CGI-Programm realisiert wird. In Kombination bewirken beide Direktiven, dass alle Zugriffe auf Dateien mit der Endung .php auf das CGI-Programm /cgi-bin/php redirected werden. Dies erzeugt als Nebenwirkung genau die benötigte $_SERVER['REDIRECT_STATUS']-Variable.

Die beiden Apache-Konfigurationsdirektiven bewirken, dass Dateien mit der Endung .php im PHP-Interpreter landen und die Abfrage auf die $_SERVER['REDIRECT_STATUS']-Variable bewirkt, dass ein direkter Aufruf des PHP-Interpreters mit einem manipulierten $_SERVER['PATH_INFO'] nicht mehr möglich ist. In Kombination stellen beide Mechanismen ein vernünftiges Maß an Sicherheit her.

PHP berechnet nun den Wert der Variablen $_SERVER['PHP_SELF'] auf andere Weise, falls enable-force-cgi-redirect in Kraft ist: In diesem Fall enthält die Variable wirklich nur den Namen des Scriptes ohne den Pfad zum PHP-Interpreter. Ist der Interpreter jedoch ohne diese Einstellung übersetzt worden, ist der Name des PHP-Interpreters Bestandteil von $_SERVER['PHP_SELF']. Dies ist ein Hinweis auf ein schweres Sicherheitsproblem. Der Interpreter sollte ausgetauscht werden durch eine Version, die mit enable-force-cgi-redirect übersetzt wurde.

Manche Webserver sind nicht in der Lage, Endungen auf CGI-Programme zu mappen (beim Netscape Server kann man dies mit Hilfe eines Plugin-Modules nachrüsten) oder sie erzeugen keine $_SERVER['REDIRECT_STATUS']-Variable, wenn sie ein solches Mapping vorgenommen haben. In diesem Fall muss man ohne enable-force-cgi-redirect arbeiten und mit dem Sicherheitsloch leben oder - besser - den Webserver wechseln.

4.6. Warum funktioniert set_time_limit() nicht wie angepriesen?

Antwort von Kristian Köhntopp

Die Funktion set_time_limit() bzw. die Konfigurationsanweisung max_execution_time in der php.ini wirkt nicht auf die absolute Laufzeit des Scriptes, sondern sie begrenzt die verbrauchte CPU-Zeit eines Scriptes. In

  set_time_limit(1);
  sleep(10);
  print("hallo");

verbraucht die sleep() -Funktion zwar reale Zeit, aber keine CPU-Zeit. Daher wird das Zeitlimit von einer Sekunde hier auch nicht wirksam und der Text wird noch gedruckt.

4.7. Apache: Kann ich PHP auch auf .html-Dateien anwenden?

Keywords: phps | html | Syntax | Highlighting | Parser | Apache | Datei | Endung

Antwort von Johannes Frömter

Prinzipiell kann man Dateien mit beliebigen Endungen durch PHP verarbeiten lassen, also auch die Endung .html. Normalerweise braucht man hierzu Zugriff auf die Apache-Konfigurations-Datei httpd.conf (wenn der Provider es nicht unterbunden hat, reicht aber auch ein Eintrag in einer .htaccess-Datei, die man einfach in sein Home-Directory stellt). Auf diese Weise lassen sich auch prima in PHP3 geschriebene Anwendungen bei einem Provider betreiben, der nur noch PHP4 anbietet (die Kompatibilität beträgt praktisch 100%, siehe Anhang B. des PHP Manuals).

Je nachdem, ob PHP in der Modul- oder der CGI-Version zum Einsatz kommt, unterscheiden sich die Konfigurationsanweisungen. Bei PHP4 ist das Modul die häufigere Art, hier sieht die Zeile meist so aus:

AddType application/x-httpd-php .php .php4 .html

Mehrere Dateiendungen schreibt man einfach durch Leerzeichen getrennt hintereinander. Beim Massenhoster 1&1 (PureTec, S+P, sowie deren Reseller) ist bei AddType eine etwas andere Syntax notwendig: AddType x-mapp-php4 .php .html

Diese Konfigurationsanweisungen können in der httpd.conf entweder in Section 2 ('Main' server configuration) stehen, in <VirtualHost>-Containern (Section 3), wo sie nur für den entsprechenden Domain-/Hostnamen gelten, oder in <Directory>-Containern, wo sie nur für ein bestimmtes Verzeichnis und dessen Unterverzeichnisse gelten. Auch in .htaccess-Dateien sind sie möglich, sie gelten dann ebenfalls für nur für ein Verzeichnis bzw. dessen Unterverzeichnisse.

Betreibt man PHP nicht als Webserver-Modul, sondern als CGI (z.B. PHP3 parallel zu PHP4), sieht der Eintrag folgendermaßen aus:

AddType application/x-httpd-php3 .php3 .html
Action  application/x-httpd-php3 /cgi-bin/php

// Für Windows
ScriptAlias /php3/ "/path-to-php-dir/"
AddType application/x-httpd-php3 .php3
Action  application/x-httpd-php3 "/php3/php.exe"

Solange die Modulversion von PHP verwendet wird und in den verarbeiteten Dateien keine PHP PIs (SGML Processing Instructions, die PHP einschalten: <?php usw.) vorkommen, ist der Mehraufwand in CPU-Belastung und Zeitverbrauch kaum meßbar. Bei der CGI-Version entspricht der Mehraufwand dem Ausliefern jeder HTML-Seite durch ein CGI-Programm. Das kann beträchtlich sein.

Wichtig: Bei Änderungen an der httpd.conf ist ein Neustart des Apache notwendig (Linux: apachectl restart), um sie wirksam werden zu lassen.

Eine besondere Funktion kann man der Dateiendung .phps zukommen lassen, PHP stellt dann den Quellcode der Datei mit farbig hervorgehobenen Tags dar:

AddType application/x-httpd-php-source .phps

Man kann seine PHP-Dateien kopieren und als .phps speichern, dies hat jedoch Nachteile: es wird der doppelte Speicherplatz verbraucht, und bei Änderungen an der Datei muss die Kopie auch aktualisiert werden. Unter Unix/Linux kann man einen symbolischen Link auf die Datei anlegen, um dies zu vermeiden (ln -s file.php file.phps). Allerdings muss dies für jede Datei getan werden; will man alle Dateien mit der Endung .php auch als .phps abrufen können, bastelt man mit dem Apache-Modul mod_rewrite (oder auch mod_alias) eine Regel, die diese Requests abfängt und an ein PHP-Skript weiterleitet. Im Manual bei der Funktion highlight_file() findet man ein Beispiel, wie man Source Highlighting auch auf recht elegante Art mittels ForceType erreichen kann.

4.8. Wie kann ich PHP (CGI und Apache-Modul) konfigurieren?

Antwort von Johannes Frömter

PHP verwendet die globale Konfigurationdatei php.ini. Die Ausgabe von phpinfo() sagt, wo PHP diese Datei sucht. Findet PHP keine php.ini, arbeitet es mit Default-Werten, die sich kaum von denen aus der php.ini-dist unterscheiden. Ob bzw. welche Konfigurationsdatei verwendet wird, läßt sich mittels

echo get_cfg_var("cfg_file_path");

herausfinden. Neben der php.ini gibt es aber noch andere Möglichkeiten, die PHP-Konfiguration zu beeinflussen:

httpd.conf (zentrale Apache-Konfiguration)

Diese Einstellungen wirken sich auf den ganzen Server (oder virtuelle Hosts oder sog. Container) aus (nur bei PHP als Apache-Modul):

# String- und Integerwerte
# php_value variable wert
php_value max_execution_time 60

# Booleanwerte
# php_flag variable on|off
php_flag track_vars on

# Geschützte Werte, die nur in der zentralen
# Apache-Konfig gesetzt werden dürfen.
# php_admin_value variable wert
# php_admin_flag  variable on|off
.htaccess (Per-Verzeichnis-Konfigurationsdatei)

php_flag und php_value können wie oben beschrieben verwendet werden. Diese Einstellungen wirken sich auf alle PHP-Scripts im aktuellen und allen Unterverzeichnissen aus (nur bei PHP als Apache-Modul).

ini_set (Konfiguration im PHP-Script)

Mit ini_set() kann man zur Laufzeit des Scripts die PHP-Konfiguration manipulieren:

ini_set("max_execution_time", "60");
phpinfo();

Wie man sehen kann, wurde die maximale Ausführungszeit für dieses eine Script verdoppelt (gegenüber der Default-Einstellung).

Diese drei Einstellmöglichkeiten betreffen jeweils den sog. "Local Value", im Gegensatz zum "Master Value", der ausschließlich in der php.ini definiert wird. Effektiv zur Anwendung kommt der "Local Value". Eine Übersicht über die Werte erhält man mit phpinfo() .

Hinweis: Bei Änderungen an den Dateien httpd.conf und php.ini muss bei der Modulversion von PHP der Apache neu gestartet werden, um die Änderungen wirksam werden zu lassen.

4.9. Ich habe eine Frage zur Webserver-Konfiguration

Antwort von Johannes Frömter

Fragen zur Webserver-Konfiguration, soweit sie nicht unmittelbar etwas mit PHP zu tun haben, werden am besten in der dafür vorgesehenen Newsgroup de.comm.infosystems.www.servers behandelt. Für diese Newsgroup existiert auch eine kleine FAQ, die vielleicht die ein oder andere Frage schon im Vorfeld klären hilft.

4.10. Meine Änderungen mit ini_set() haben keine Wirkung. Wie kann ich Konfigurationsvariablen zur Laufzeit ändern?

Antwort von Stephan Mann

Die einfachste Möglichkeit, Konfigurationsvariablen zu ändern, ist, diese in der php.ini einzutragen (siehe hierzu Wo finde ich die php.ini?). Dies ist aber nicht immer möglich bzw. gewünscht. Entweder weil man keinen Zugriff auf die php.ini hat oder weil man serverunabhängig programmieren möchte. Deshalb gibt es die Möglichkeit, Konfigurationsvariablen zur Scriptlaufzeit zu ändern. Eine volle Übersicht über die verfügbaren Variablen gibt es auf der Manual-Seite der Funktion ini_set() .Besondere Aufmerksamkeit verdient hier die dritte Spalte der Tabellen. Variablen, die mit PHP_INI_ALL gekennzeichnet sind, lassen sich mit der Funktion ini_set() innerhalb eines Scriptes ändern oder in einer .htaccess-Datei setzen. So kann man zum Beispiel innerhalb eines Scriptes und unabhängig vom Server-Betriebssystem den Include-Path anpassen:

    $include_path[] = ".";
    $include_path[] = "pear/";
    
    ini_set("include_path", implode(strpos($_SERVER['SERVER_SOFTWARE'], 'Win') 
      ? ';' 
      : ':', 
      $include_path
    ));

Mit PHP_INI_PERDIR gekennzeichnete Variablen lassen sich ausschließlich mittels einer .htaccess-Datei setzen. So ist auf manchen Servern per default session.use_trans_sid deaktiviert. Folgende .htaccess-Datei aktiviert dies:

    <IfModule mod_php4.c>     php_value session.use_trans_sid 1</IfModule>

Um mit PHP_INI_SYSTEM gekennzeichnete Variablen zu ändern, benötigt man Zugriff auf die php.ini.

5. Fragen zum PHP Interpreter

5.1. Wie vergleicht sich PHP mit anderen bekannten Webentwicklungssystemen?

Antwort von Kristian Köhntopp

PHP ist ein Bytecode-Compiler, der das Script beim Aufruf compiliert. PHP kann als CGI-Programm oder als Bestandteil des Webservers (mod_php) ausgeführt werden. Als CGI-Programm ist es beliebig portabel, als Modul ist es für eine Reihe von populären APIs verfügbar. PHP unterstützt Datenbankzugriffe nicht nur über ODBC (oftmals Treiber von schlechter Performance und mit unvollständigen API-Implementierungen), sondern auch über die Native API einer ganzen Reihe von Datenbanken. Dazu LDAP, IMAP und eine Reihe anderer Datenbanken, außerdem HTTP, FTP-Protokolle (Intelligent Agent-Programmierung) und Direktzugriff auf Dbase und DB/DBM-Dateiformate. Dynamische Generierung von PNG-Bildern mit der libgd und der freetype library. Volltextindices und Suchmaschinen über externe Open Source Programme (MySQL Fulltext und andere). Gateways nach Java und COM existieren und werden mitgeliefert. Zahlreiche Spracherweiterungen vorhanden, der Quelltext des Interpreters liegt vor und ist Open Source. Die Syntax folgt der C, Java, Javascript, Perl-Familie von Sprachen. Ausgezeichnete Dokumentation und glänzender Support, wie bei Open Source üblich (Mailinglisten in Deutscher und Englischer Sprache). Sessionmanagement seit Version 4 eingebaut.

Cold Fusion ist eine Interpreter-Markup-Language. Es handelt sich um eine reine Interpretersprache mit der Option, Seitenquelltext auf der Serverseite durch "Codierung" zu verschleiern (das ausgegebene HTML muss selbstverständlich lesbar sein). Cold Fusion kann als Modul in Apache ausgeführt werden oder als CGI-Programm. Da es nicht Open Source ist, ist seine Verfügbarkeit über Plattformgrenzen beschränkt: Ursprünglich nur auf Windows verfügbar, ist Linux-Support mittlerweile für einige Distributionen vorhanden; Solaris wird ebenfalls unterstützt. Datenbankzugriff erfolgt über ODBC. LDAP, IMAP und einige andere Dinge werden unterstützt, andere Protokolle müssen ggf. über Dritthersteller oder Erweiterungen von Herstellerseite zugekauft werden. Intelligent Agents können erstellt werden. Keine Generierung von GIF-Bildern on-the-fly. CF kann Texte mit einer eigenen Engine (Verity) volltextindizieren und dann darauf zugreifen. Zahlreiche Spracherweiterungen vorhanden (Custom Tag Library beim Hersteller). Syntax: Tagschreibweise, um bei Anfängern den Eindruck einer Programmiersprache zu vermeiden. Neuere Versionen von Cold Fusion haben Java- und COM-Interfaces.

Microsoft ASP ist keine Scriptsprache, sondern ein Framework für die Einbindung einer solchen. Es stellt mehr eine API dar, die beliebige Scriptsprachen verwenden können, um Variablen über die Lebensdauer einer Seite hinaus persistent zu machen, um mit dem Webserver und anderen Scriptsprachen kommunizieren zu können und um andere, Microsoft-spezifische Eigenheiten ansprechen zu können. Mit MS-ASP werden zwei Scriptsprachen mitgeliefert (JavaScript und VisualBasic), aber es existieren weitere, von Microsoft unabhängige Sprachen (etwa Perl), die ebenfalls ASP verwenden. ASP ist fester Bestandteil des IIS auf NT bzw. Windows 2000/XP, es gibt aber auch andere Webserver (z.B. Sambar Server), die ASP unterstützen. Da es sich beim IIS mit ASP um ein Microsoft-Produkt handelt, sind Supportoptionen, -preise und -Qualität sofort klar (Hinweis: Seit Windows 2000 verlangt Microsoft eine zusätzliche Internet Connection License für den IIS, wenn dieser am Internet betrieben werden soll). Unterstützung anderer Plattformen ist kaum möglich, da ASP starken Gebrauch von den OLE/COM/DCOM-Fertigkeiten der MS-Plattform macht: Selbst nach der Portierung (etwa durch Chilisoft oder Software AG) steht man immer noch vor dem Problem, die entsprechenden OLE/COM/DCOM-Objekte auf der Nicht-Microsoft Plattform verfügbar zu machen. LDAP, IMAP und andere Dinge sind über den OLE/COM/DCOM-Mechanismus möglich, entsprechende Objekte sind eventuell von Drittanbietern zu kaufen.

mod_perl und embedPerl sind ein Mechanismus, mit denen man den Perl-Interpreter als Bestandteil des Apache-Webservers ausführen kann und mit dem man dann Perl-Programme mit HTML mischen kann. Programme werden proaktiv geladen und compiliert, sodass die typische Startup-Latenz von Perl-Programmen als CGI wegfällt. Der Speicherverbrauch ist beträchtlich, die Geschwindigkeiten von PHP und und mod_perl liegen in etwa gleichauf. Unterstützt wird der Apache Webserver auf allen Plattformen. mod_perl ist prinzipiell in der Lage, alle Perl-Module auf dem CPAN auszuführen, daher werden alle von Perl unterstützten Datenbanken (einschließlich ODBC und einer Menge nativer Interfaces) unterstützt, ebenso LDAP, IMAP, LibGD. Syntax folgt der von Perl... Negativ fällt hier vor allen Dingen der übermäßige Speicherverbrauch bei größeren Projekten auf und die auf Anfänger abschreckend wirkende Vielfalt von Sprachkonstrukten und Bibliotheken. Besondere Pluspunkte für erfahrene Programmierer sind die Ausdrucksstärke und Vielfalt der Sprache und ihrer Konstrukte und die wirklich umfassende Sammlung von Bibliotheken.

Apple Webobjects sind ein System, mit dem ein kleines, im Source vorliegendes Rumpf-C-CGI-Programm über einen remote procedure call mit einem als Coprozess laufenden Anwendungsprogramm kommuniziert. Das Anwendungsprogramm ist mit dem Webobjects Toolkit in Objective-C geschrieben und wird dann compiliert; es ist ein Maschinenprogramm. Kommunikation erfolgt durch das Datenbank-Kit von Apple über die Native API der unterstützten Datenbanken mit beliebigen Datenbanken. Apple bietet außerdem eine exzellente Entwicklungsumgebung mit GUI-Designer (ja, erzeugt auch HTML :-) ) und einem sehr schönen ER-Modeller und Debugger (gdb mit einem sexy Outfit). Webobjects skaliert sich ausgezeichnet durch das verwendete RPC-Schema (Apple Portable Distributed Objects auf einem beliebigen etablierten RPC-Mechanismus aufsetzend) und die Möglichkeit, den Application Server zu replizeren. Interessant ist die ungeschlagene Geschwindigkeit und die extrem gute Entwicklungsumgebung.

5.2. Wie vergleicht sich die Performance von PHP zu Perl?

Antwort von Kristian Köhntopp

PHP erzeugt wie Perl beim Start des Programms Bytecode und führt diesen dann aus. Dabei liegt PHP mit Perl geschwindigkeitsmäßig in etwa gleichauf. Ebenso wie Perl braucht auch PHP dafür eine größere Startup-Zeit, in der das Programm analysiert und übersetzt wird. Mit einem Bytecode-Cache kann man diese Zeit senken, indem man PHP Speicher zum Caching für Bytecode bereitstellt.

Der Zend-Optimizer und viele andere Bytecode-Caches optimieren den PHP-Bytecode noch einmal und holen je nach Anwendung Geschwindigkeitsgewinne heraus. Diese Gewinne können in Benchmarks signifikant sein - ihre praktische Bedeutung wird ebenfalls merkbar sein, aber sicherlich nicht so extrem wie in den Benchmarks. Die Anwendung des Optimizers würde die Startup-Zeiten des PHP-Interpreters noch weiter erhöhen, deswegen werden sie in der Regel mit einem Bytecode-Cache zusammen verwendet. Der Zend-Optimizer ist ein closed source Produkt, aber es existieren auch OSS-Alternativen.

Ein Bytecode-Cache kann häufig verwendeten Bytecode erkennen und im Interpreter im Speicher halten. Der Interpreter braucht diesen Bytecode dann nicht mehr zu laden, zu analysieren und zu übersetzen, sondern kann ihn direkt aufrufen. Durch den Bytecode-Cache erhöht sich der Speicherverbrauch des Interpreters und bei unzureichendem Speicherausbau der Maschine kann sich das negativ auf die Performance des Gesamtsystems auswirken.

Man sagt, dass PHP leichter zu erlernen sei als Perl und dass PHP-Code leichter zu lesen und damit billiger zu warten wäre als Perl-Code. Das ist sicherlich eine Frage der Übung - man kann in beiden Sprache nicht mehr wartbare Programme entwickeln bzw. in beiden Sprachen selbstdokumentierenden Code abliefern.

5.3. Wie kann ich mein ASP-Programm in PHP übersetzen?

Antwort von Kristian Köhntopp

Mit Hilfe des Konverters asp2php kann man große Teile seiner ASP-Anwendung zunächst einmal in PHP übersetzen lassen. In vielen Fällen sind kleinere Nacharbeiten oder wenigstens Nachkontrolle notwendig.

5.4. CGI PHP oder Modul?

Antwort von Kristian Köhntopp

Siehe auch Webserver verstehen und tunen von Kristian Köhntopp.

Jedes Mal, wenn eine Seite mit PHP-Code darauf ausgeführt werden muss, muss der PHP-Interpreter gestartet werden. Wird PHP als CGI-Programm installiert, bedeutet dies, dass am Anfang der Seite ein neuer Prozess erzeugt werden muss und der PHP-Interpreter in diesen Prozess geladen werden muss. Dies verbraucht eine ganze Menge Systemressourcen. Am Ende der Seite endet auch der Interpreterprozess und aller Speicher wird freigegeben. Ebenso werden alle Filehandles geschlossen und damit alle Datenbankverbindungen aufgegeben.

Installiert man PHP dagegen als Modul, etwa in einem Apache-Webserver, dann ist das PHP-Modul Bestandteil des Webservers und ständig geladen. Es kann außerdem Datenbanklinks über die Lebensdauer einer PHP-Seite hinaus offen halten, was speziell bei Oracle-Datenbanken große Performancevorteile bringt.

Nur mit einem CGI-PHP ist es möglich, PHP als Scriptsprache zur Erstellung von "Shellscripten" einzusetzen.

Viele Massen-Webhoster setzen bevorzugt CGI-PHP ein, weil es sich leichter auf eine Weise installieren lässt, die die Systemsicherheit nicht gefährdet. CGI-PHP erlaubt den Einsatz der Apache-Erweiterung suexec, die Erstellung einer chroot()-Umgebung und die bessere Kontrolle der durch den Anwender verbrauchten Systemressourcen.

Praktisch alle großen PHP-Sites setzen PHP als Modul ein, weil die Performance hier deutlich besser ist.

5.5. Portable PHP-Scripte

Antwort von Kristian Köhntopp

PHP ist weitgehend unabhängig von der verwendeten Systemplattform. Von den Funktionen microtime() und crypt() ist bekannt, dass sie sich beiden Betriebssystemen unterschiedlich verhalten.

Einige PHP-Module stehen nicht unter allen Systemplattformen zur Verfügung. So ist das Posix-Modul systembedingt nur unter Unix sinnvoll einsetzbar und das DCOM-Modul nur unter Windows verfügbar.

PHP unterscheidet in Funktionen nicht zwischen den verschiedenen Verzeichnistrennern / und \. Man sollte jedoch beachten, dass ein Backslash in Strings als \\ geschrieben werden muss. Möchte man erfahren, wie der Verzeichnistrenner auf einem System heißt, kann man die vordefinierte Konstante DIRECTORY_SEPARATOR benutzen.

5.6. Zeitgesteuerte PHP-Scripte und Shellscripte

Antwort von Kristian Köhntopp

"echo" als PHP-Script "echo.php":

#! /usr/bin/php
<?php
  echo "argc = {$_SERVER['argc']}\n";
  foreach ($_SERVER['argv'] as $k => $v) {
    echo "_SERVER['argv'][$k] = $v\n";
  }
?>

Dies setzt ein installiertes Binary von PHP in /usr/bin/php voraus. Seit PHP 4.3.0 wird PHP (ob Webserver-Modul oder CGI-Version) immer mit dem CLI (Command Line Interface) ausgeliefert (UNIX/Linux: ein Binary namens "php", Windows: "php.exe"). Dieses kann für Standalone-Applikationen benutzt werden. Zusätzlich bietet das CLI noch einige weitere interessante Optionen und Eigenschaften

Ein solches Script lässt sich über die Unix-Zeitsteuerung cron bzw. äquivalente Programme regelmäßig aufrufen.

Dem Script steht das Array $_SERVER['argv'][] zur Verfügung, welches die Kommandozeilenparameter des Aufrufs enthält. Dieses Array kann auf die übliche Weise aufgezählt werden. Die Anzahl der Elemente des Arrays kann man mit der Funktion count() bestimmen oder in der Variablen $_SERVER['argc'] nachschlagen.

Hat man nur mod_php zur Verfügung, kann man eine bestimmte URL des Webservers durch PHP regelmäßig zeitgesteuert abrufen lassen. Dazu sind Tools wie wget oder lynx hilfreich.

5.7. Wie bette ich PHP in HTML ein? (Beispielprogramm)

Antwort von Kristian Köhntopp

PHP-Code wird mit Hilfe von SGML Processing Instructions (PIs) in HTML eingebettet. Der Code steht zwischen <?php und ?> (empfohlene Schreibweise):

<?php
  phpinfo();
 ?>

Alternativ kann auch ein Script-Tag für Server-Side Scripting verwendet werden:

<script language="php">
  phpinfo();
</script>

Falls die Konfigurationsvariable short_open_tag gesetzt ist (nicht empfohlen!), kann man den Namen der Scriptsprache in den PIs weglassen:

<?
  phpinfo();
 ?>

Dies erlaubt auch schnelle Ausgabe des Wertes von Ausdrücken.

<?= $_SERVER['PHP_SELF'] ?>
<!-- kann anstelle von -->
<? echo $_SERVER['PHP_SELF'] ?>
<!-- geschrieben werden. -->

Falls die Konfigurationsvariable asp_tags gesetzt ist (nicht empfohlen!), kann man auch

<%
  phpinfo();
 %>

a la Microsoft schreiben. Dies wird von vielen Editoren (Frontpage, Dreamweaver) besser verstanden.

5.8. Wie kann ich auf Umgebungsvariablen zugreifen?

Antwort von Kristian Köhntopp

Man kann Umgebungsvariablen über die PHP-Einbaufunktion getenv() eine solche Variable lesen und sie mit Hilfe der PHP-Einbaufunktion putenv() setzen. Dies ist die empfohlene Methode.

Alternativ sind Umgebungsvariablen sind innerhalb von PHP als Variablen im Array $_ENV verfügbar. Die Umgebungsvariable HOME steht also als Element im Array $_ENV["HOME"] zur Verfügung.

<?php
  function somefunc() {
    // Empfohlen
    echo getenv("HOME"). "<br />\n";

    // $_ENV ist superglobal und automatisch in
    // Funktionen verfügbar.
    echo $_ENV["HOME"]<br />\n";
  }

  somefunc();
 ?>

Mit Hilfe der Funktion phpinfo() bekommt man unter anderem auch eine Übersicht über alle diese Variablen.

5.9. Wie kann ich auf den HTTP-Request-Header zugreifen?

Antwort von Kristian Köhntopp

Allen CGI-Programmen stehen zusätzliche Zeilen des HTTP-Request-Headers als Umgebungsvariablen zur Verfügung. Die Headerzeile bla-fasel: laber wird dabei zur Umgebungsvariablen $_ENV["HTTP_BLA_FASEL"] mit dem Wert laber.

Zusätzliche Angaben hinter der URL der Seite stehen in der Umgebungsvariablen $_ENV["PATH_INFO"] zur Verfügung. So wird im Request http://example.com/test.php/additional/info der Anteil /additional/info in dieser Variablen zu finden sein.

Zusätzliche Angaben in der Form von Query-Strings wie sie von Formularen mit der METHOD="get" erzeugt werden, stehen in der Variablen $_SERVER["QUERY_STRING"] zur Verfügung. Ist der Query-String korrekt formuliert, werden diese Parameter auch als Variablen im Array $_GET vorbelegt. So wird der Request http://example.com/test.php?a=b&c=d die Umgebungsvariable $_SERVER["QUERY_STRING"] mit dem Wert a=b&c=d belegen und außerdem die Variablen $_GET["a"] mit dem Wert b und die Variable $_GET["c"] mit dem Wert d vorbelegen.

Verwendet man PHP als Apache-Modul, so kann man außerdem noch über die spezielle Funktion getallheaders() auf diese Request-Header zugreifen. Ein solches Script ist jedoch nicht mit einem CGI-Interpreter ausführbar, daher sollte man diese Funktion im Namen der Portabilität vermeiden.

5.10. Gibt es noch mehr interessante Variablen im Environment?

Antwort von Kristian Köhntopp

Der CGI-Standard definiert eine Reihe von Variablen, die vorhanden sein müssen. Eine Übersicht bekommt man wie üblich mit phpinfo() Von besonderem Interesse sind

$_SERVER["HTTP_REFERER"]

Die URL der Seite, die auf diese Seite verwiesen hat.

$_SERVER["HTTP_USER_AGENT"]

Die Browserkennung des abrufenden Browsers.

$_SERVER["REMOTE_ADDR"]

Die IP-Nummer des abrufenden Rechners. Dies kann die momentane IP-Nummer des tatsächlichen Abrufers (oft dynamisch vergeben und variabel) oder die IP-Nummer des Proxy-Servers sein, den der Abrufer verwendet.

5.11. Wie kann ich auf Kommandozeilen-Argumente zugreifen?

Keywords: shell | Parameter | CGI | Kommandozeile | Script

Antwort von Kristian Köhntopp

Wenn PHP über die Shell als Skriptsprache benutzt wird, ist es oft nützlich, auf der Kommandozeile Parameter zu übergeben. In PHP stehen die Variablen $_SERVER["argc"] und $_SERVER["argv"] zur Verfügung.

$_SERVER["argc"]

Anzahl der auf der Kommandozeile übergebenen Argumente.

$_SERVER["argv"]

Array mit den übergebenen Argumenten und dem Dateinamen im ersten Element.

Wenn ein PHP-Skript über das Web aufgerufen wird, enthalten diese Variablen die über GET übergeben Argumente. Man kann dieses Verhalten in der php.ini-Datei abschalten ( register_argv_argc ).

Beispiel:

tobias@dev:~ > cat arg.php
#! /usr/bin/php -q
# Getestet mit Suse Linux 8.1 (register_globals = off)
<?php
  echo "argc = {$_SERVER['argc']}\n";
  foreach ($_SERVER['argv'] as $k => $v) {
    echo "_SERVER['argv'][$k] = $v\n";
  }
tobias@dev:~ > ./arg.php foo bar baz
argc = 4
_SERVER['argv'][0] = /var/tmp/x.php
_SERVER['argv'][1] = foo
_SERVER['argv'][2] = bar
_SERVER['argv'][3] = baz

Antwort von Frank Wiegand

Komfortables Parsing der Argumente bietet das PEAR-Paket Console_Getopt.

5.12. Wie kann ich einen Parameter von einer PHP-Seite an eine andere weitergeben?

Antwort von Kristian Köhntopp

Man kann Parameter an ein PHP-Script als HTTP-GET- oder HTTP-POST-Parameter übergeben. Die Übergabe von Parametern als HTTP-POST ist in Wie kann ich einen HTTP POST-Request absenden? erläutert.

Einen HTTP-GET-Request erzeugt man, indem man einfach ein Link auf das gewünschte Script erzeugt und die Parameter mit der Funktion urlencode() codiert anhängt.

<?php

 $para1 = "dies ist ein string";
 $para2 = 42;

 $pstring = sprintf("para1=%s&amp;para2=%s",
		urlencode($para1),
		urlencode($para2));
?>
<a href="meinscript.php?<?php print $pstring ?>">go</a>

Das empfangende Script wird diese Parameter ganz normal entgegennehmen, automatisch decodieren und als Variablen mit den Namen $_GET["para1"] und $_GET["para2"] bereitstellen.

Die Länge der durch einen GET-Request übergebaren Parameter ist begrenzt. Im einem GET- oder POST-Request übergebene Parameter sind durch den Anwender leicht manipulierbar. Wie in Webserver verstehen und tunen diskutiert, ist es wesentlich besser, Sessionvariablen zu verwenden, wie sie durch Sessionfunktionen von PHP4 realisiert sind - im Gegensatz zu dem hier gezeigten manipulierbaren Verfahren sind Sessions nämlich sicher.

5.13. Wie kann ich eine PHP-Präsentation auf CD brennen?

Antwort von Kristian Köhntopp

PHP ist zur Ausführung auf einen Webserver angewiesen. Es ist zwar möglich, eine PHP-Präsentation mittels eines Spiders durchzugehen und das generierte HTML abzuspeichern, aber die Interaktivität und die Datenbankverbindungen gehen so verloren. Eine solche Offline-Version einer Website lässt sich z.B. mit einem der folgenden Tools herstellen:

  • HTTrack (Windows)

  • WebZIP (Windows)

  • Wget (Unix/Linux: wget --mirror -k -E http://www.example.com/)

Alternativ kann man dem Kunden einen Webserver mitliefern, den dieser dann auf seinem Rechner installieren muss und der dann die PHP-Scripte ausführt. Der Kunde wird dann unter der URL http://localhost/ auf die Anwendung zugreifen können.

Antwort von Johannes Frömter

Von IndigoSTAR Software gibt es den Webserver MicroWeb, der direkt von CD-ROM unter Windows gestartet wird. Man kann einen beliebigen Hostnamen definieren und per HTTP auf die Daten zugreifen (z.B. http://microweb/). Standardmäßig bringt MicroWeb Perl- und SSI-Unterstützung mit, man kann aber auch PHP (als CGI) einbinden (siehe Dokumentation). Natürlich sind auf der CD keine Datenbank- und Dateioperationen möglich; für letztere besorgt man sich am besten via getenv("TEMP") das temporäre Verzeichnis des Rechners und arbeitet damit auf der Festplatte.

5.14. Werden meine PHP-Seiten von einer Suchmaschine indiziert?

Antwort von Kristian Köhntopp

Ein Webspider bekommt bei der üblichen Konfiguration von Webservern in keinster Weise mit, ob die angeforderte Default-Datei (Directory-Index) eines Verzeichnisses nun home.htm, index.php oder irgendwie anders heißt.

Er wird diese Datei lesen, und dann werden die HTML-Tags extrahiert, die für das Ranking aber noch eine Rolle spielen können (<h1>...</h1>). Aus dem reinen Text werden meistens noch die Stopwörter entfernt und restliche Textbrei fließt dann aufbereitet in den Index. Die Meta-Tags spielen bei intelligenteren Suchmaschinen für das Ranking keine Rolle mehr.

Jede Suchmaschine könnte grundsätzlich dynamisch generierte Seiten genauso erfassen, wie statische Seiten, weil der Spider/Robot der Suchmaschine genauso ein Client ist, wie Dein Browser und nicht mehr und nicht weniger sieht als Dein Browser: nämlich den HTML-Code und den Content-Type. Endungen der Dateinamen spielen bei korrekt programmierten Suchmaschinen keine Rolle - entscheidend sind im Web stattdessen die übermittelten Content-Types.

Viele Suchmaschinenbetreiber werden jedoch keine dynamisch generierten Seiten erfassen, weil sie davon ausgehen, dass sich deren Inhalt sehr oft ändert und eine Indizierung der Seiten somit sinnlos ist. Wird nun bei einem HTML-Dokument aufgrund der Extension, des Pfadnamens (enthält das Schlüsselwort cgi) oder offensichtlich per GET übergebener Parameter eine dynamische Generierung vermutet, werden diese Dateien von einigen Spidern nicht indiziert, bzw. entsprechende Links nicht verfolgt. Dies ist zwar ebenfalls falsch - stattdessen sollte sich der Spider nur nach dem Inhalt der Datei robots.txt richten - aber viele Sites haben nur ungenügende oder ganz fehlende robots.txt-Dateien.

Lies auch den Artikel von Tobias Ratschiller in der Suchfibel zu diesem Thema.

5.15. Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?

Antwort von Martin Jansen

Wenn der Webserver ordnungsgemäß konfiguriert ist und er PHP unterstützt, bekommt der Besucher der Seite den PHP-Code nicht zu sehen, da er vom Webserver geparst wird und nur der generierte HTML-Code an den Browser geschickt wird.

Folgende Szenarien sind jedoch denkbar, bei denen der Besucher den PHP-Code trotzdem zu sehen bekommt:

  • Auf dem Webserver ist kein PHP-Parser installiert. In diesem Fall behandelt der Webserver den PHP-Code wie "normalen" HTML-Code und schickt ihn unbearbeitet (ergo ungeparst) an den Browser.

  • Aufgrund einer Fehlfunktion des Webserver ist der PHP-Parser ausgefallen oder der Webserver benutzt ihn nicht mehr: In diesem Fall sieht der User ebenfalls den ungeparsten Source-Code im Browserfenster, da kein Parsing mehr stattfindet.

  • Ein "böser Junge" verschafft sich per FTP Zugriff auf die Daten: Auch hier hat der User die Möglichkeit, den Source-Code einzusehen, da der PHP-Parser nur Dateien parst, die über den Webserver abgefragt werden. Andere Schnittstellen wie zum Beispiel FTP berücksichtigt er nicht.

  • Ein User verschafft sich Zugriff auf den Webserver mit Telnet oder SSH: Auch hier kann er den PHP-Code ohne weiteres einsehen, indem er zum Beispiel cat irgendwas.php eingibt und der Inhalt der Datei irgendwas.php auf dem Bildschirm ausgegeben wird.

Siehe auch: Kann ich PHP-Dateien kompilieren und so vor Dritten schützen?

In jedem Fall ist es sinnvoll, Code mit Datenbankpassworten in einem Verzeichnis abzulegen, das nicht durch eine URL erreichbar ist oder das mit einer .htaccess-Datei gegen Zugriff gesichert ist. Dieser Code kann dann mit Hilfe von include() oder require() eingebunden werden. Siehe dazu auch den Abschnitt Wie kann ich mein Datenbankpasswort gegen Spionage sichern? dieser FAQ.

5.16. Wie kann ich die Ausgabe meines Scriptes in einen anderen Frame umlenken?

Keywords: Frame | target | Fenster | umlenken | Ausgabe | Pop-Up

Antwort von Daniel T. Gorski

Wenn das Script bereits läuft, gar nicht.

Es gibt einen inoffiziellen Lösungsansatz seitens Netscape, mit dem es möglich ist, die Ausgabe eines laufenden Scriptes in einen anderen Frame umzulenken. Dies ist jedoch nicht portabel: PHP läuft auf dem Server und weiß zunächst einmal nichts von den Frames eines Clients.

Es jedoch möglich, sich mit JavaScript eine Brückenseite zu schreiben (vom laufendem Script schreiben zu lassen), die mit Hilfe von onLoad() erneut eine PHP-Seite vom Server für einen anderen Frame anfordert (top.anderesFrame.location.href="seite1.php"; self.location.href="seite2.php").

Von diesem Verfahren wird hier deutlich abgeraten, da viele User (bei manchen Firmen gehört das auch zu der Firmenpolicy) JavaScript aus Sicherheitsgründen abschalten. Dann bleibt der Bildschirm u.U. leer und der Besucher verwirrt. Wiederkommen wird er bestimmt nicht mehr.

Vor der Ausführung des Scriptes, ist es selbstverständlich möglich mittels <A HREF="seite1.php" TARGET="andererFrame"> das Script in einem anderem Frame (bzw. mit javascript:window.open() geöffneten Fenster) ausführen zu lassen.

5.17. Wie kann ich mit PHP die Bildschirmauflösung des Browsers herausfinden?

Antwort von Kristian Köhntopp

Das geht im allgemeinen Fall nicht. PHP wird auf dem Server ausgeführt, nicht im Browser des Zielsystems.

Falls der Browser des Zielsystems JavaScript kann, falls dieser Browser JavaScript nicht disabled hat, falls die Firewall auf dem Weg zum Zielsystem nicht JavaScript ausfiltert und falls man an geeigneter Stelle ein Formular statt eines Links verwendet, dann kannst man in diesem Formular die Auflösung des Zielsystem durch JavaScript ermitteln lassen und an seine Site zurücksenden lassen. Mit den übrigen Fällen (Auflösung des Zielsystems nicht bekannt) muss man dennoch fertig werden.

Die Tatsache, dass die Bildschirmauflösung des Zielsystems bekannt ist, hilft natürlich nicht beim Design, solange man nicht auch weiß

  • wie groß die verwendete Schrift ist,

  • ob die verwendeten Zeichensätze auf dem Zielsystem zur Verfügung stehen,

  • wie groß die Fenster auf dem Zielsystem zur Zeit gerade sind.

Man kann aus diesen Gründen davon ausgehen, dass eine auflösungsabhängige Darstellung auch dann auf mehr als der Hälfte der Zielsysteme nicht korrekt gerendert werden kann, auch wenn die Bildschirmauflösung des Zielsystems bekannt ist.

6. Typen und Funktionen

6.1. Welche Variablenarten gibt es in PHP und wie greife ich auf sie zu?

Antwort von Kristian Köhntopp

Konstanten

define(NAME, Wert);

definiert eine Konstante. Diese Konstante ist überall gültig, also nicht nur im globalen Namensraum, sondern auch in Klassen und in Funktionen.

Globale Variablen

Eine globale Variable definiert man implizit durch ihre Benutzung innerhalb des globalen Namensraumes, also einfach durch

$a = 10;

außerhalb jeder Klasse oder Funktion.

Superglobale automatische Variablen

PHP unterhält einige Arrays, die es automatisch mit Werten befüllt. Diese Arrays sind global, und sie sind anders als normale globale Variablen automatisch auch in allen Funktionen und in Klassen sichtbar, ohne dass sie mit der Anweisung global importiert werden müssen.

Es sind dies die Felder $_GET (Get-Parameter des Scriptes), $_POST (Post-Parameter des Scriptes), $_COOKIE (Cookie-Parameter des Scriptes), $_SERVER (Serverdefinierte Variablen, CGI-Umgebung), $_ENV (Umgebungsvariablen), $_REQUEST, $_FILES und $_SESSION sowie $GLOBALS (ohne führenden Unterstrich, alle globalen Variablen).

Variablen in Funktionen

In PHP hat jede Funktion ihren separaten Namensraum. In diesem Namensraum existieren nur diejenigen Variablen, die als Formalparameter der Funktion in die Funktion importiert worden sind oder die innerhalb der Funktion definiert worden sind. Diese Variablen haben die Lebensdauer des Funktionsaufrufes, d.h. sie verlieren beim Verlassen der Funktion ihren Wert.

Es ist möglich, in den Namensraum einer Funktion weitere Variablen hineinzuimportieren. Dies geschieht mit Hilfe der Anweisung global.

Beispiel:

function testfunc($para1) {
  global $glob1;

  $loc1 = 10;
  $glob1 = $glob1 + 1;

  return;
}

Diese Funktion hat zunächst einmal die Variablen $para1 und $loc1 als Parameter bzw. als lokale Variable definiert. Diese Variablen verlieren mit der schließenden Klammer "}" ihren Wert. Außerdem wird die Variable $glob1 in den Namensraum der Funktion importiert. Der Wert von $glob1 ist auch außerhalb von testfunc() sichtbar und $glob1 bleibt für die gesamte Lebensdauer des Programmes bestehen.

Weiterhin kann man innerhalb einer Funktion noch lokale Variablen definieren, die nur innerhalb der Funktion sichtbar sind, deren Wert jedoch nach dem Ende der Funktion erhalten bleibt und bei einem erneuten Funktionsaufruf erneut sichtbar wird. Dies erfolgt mit Hilfe der Anweisung static.

function testfunc2($para1) {
  global $glob1;
  static $stat1 = 0;

  $loc1 = 10;
  $glob1 = $glob1 + 1;
  $stat1 = $stat1 + 1;

  printf("Die Funktion testfunc2() wurde %s mal aufgerufen.\n",
    $stat1);
}

In diesem Beispiel ist die Anweisung static $stat1 dazu gekommen, die die Variable $stat1 als lokale, aber langlebige Variable in testfunc2() definiert und zur Vermeidung einer Warnung mit 0 initialisiert. Der Wert von $stat1 bleibt über einen einzelnen Funktionsaufruf erhalten. $stat1 zählt also im Beispiel oben die Anzahl der Aufrufe von testfunc2().

PHP definiert die superglobale Variable $GLOBALS[] vom Typ Array/Hash. Es handelt sich um einen Namen fuer die globale Symboltabelle des Interpreters. Der Name ist in allen Namensräumen sichtbar. Entsprechend ist das Konstrukt

function testfunc3() {
  global $glob1;

  $glob1 = $glob1 + 1;

  return;
}

gleichbedeutend mit

function testfunc3b() {
  $GLOBALS["glob1"] = $GLOBALS["glob1"] + 1;

  return;
}

Variablen in Klassen

Die dritte Sorte Namensräume, die in PHP existiert, sind Klassen und Objekte. Eine Klasse wird vereinbart mit der Anweisung

class MyClass {
  var $a;
  var $b;

  function c() {
    print("Der Wert von a ist %s\n", $this->a)
    $this->b = $this->b + 1;
    print("Der Wert von b ist %s\n", $this->b);
  }

  function MyClass() {
    $this->a = 10;
    $this->b = 0;
  }
}

Nach dieser Definition ist KEINE Variable belegt, aber es existiert ein Bauplan fuer MyClass-Variablen. MyClass ist ein Typ, so wie integer, float oder string Typen in PHP sind. Mit Hilfe der Anweisung "new" kann man sich Variablen nach diesem Bauplan erstellen lassen. "new" ist also eine Fabrik, der man einen Bauplan mitgibt und die nach diesem Plan Variablen herstellt.

$o = new MyClass;

Den Bauplan einer Variablen bezeichnet man als Klasse, hier der Klasse MyClass. Die nach diesem Plan gebaute Variable als ein Objekt der Klasse, hier also als das Objekt $o der Klasse MyClass. Manche Leute sagen zu dem Objekt $o der Klasse MyClass auch "$o ist eine Instanz der Klasse MyClass".

Nach einem Bauplan können mehrere Objekte gebaut werden, d.h. eine Klasse kann mehrere Instanzen haben.

$p = new MyClass;

Die Variablen mit den Namen "a" und "b" in einem Objekt der Klasse MyClass werden in PHP als Slots oder Instanzvariablen bezeichnet. Man spricht sie immer über den Namen des Objektes an:

$o->a = 17;
$p->a = 31;

Dadurch werden sie unterscheidbar, d.h. es ist erkennbar, ob man den Slot mit dem Namen "a" des Objektes $o oder des Objektes $p meint. Das kann man sich vorstellen wie Pfadnamen: In unterschiedlichen Verzeichnissen eines Dateisystems kann man ja auch Dateien mit demselben Namen liegen haben, aber diese Variablen unterscheiden sich im Pfadnamen.

Eine Klasse kann nicht nur Instanzvariablen definieren, sondern auch Instanzfunktionen, hier die Funktionen mit den Namen "c" und "MyClass". Sie werden genauso angesprochen wie die Instanzvariablen:

$o->c();
$p->c();
$o->MyClass();
$p->MyClass();

Manche Leute nennen die Funktionen eines Objektes auch "Methoden" und reden dann von "Instanzmethoden" statt "Instanzfunktionen". Das hat keinen praktischen Zweck und dient nur der Förderung der allgemeinen Verwirrung.

Beim Funktionsaufrufen gilt die folgende Sonderregel: Eine Funktion, die exakt denselben Namen hat wie die Klasse, in der sie enthalten ist, wird auch dann aufgerufen, wenn das Objekt von new gebaut wird. Diese Funktion kann also verwendet werden, um die Slots des Objektes mit Defaultwerten zu initialisieren:

$c = new MyClass;

Hier wird also die Funktion $c->MyClass() automatisch aufgerufen. Weil diese Funktion aufgerufen wird, während das Objekt nach dem Bauplan gebaut wird, nennt man eine solche Funktion einen Konstruktor. Ein Konstruktor könnte auch optionale Parameter erwarten und man könnte der Funktion dann beim Zusammenbau diese Parameter mitgeben:

class MyClass2 {

  ...

  function MyClass2($p1 = "") {
    if ($p1 != "") {
      $this->...
    }
  }
}

$mc1 = new MyClass2("beispielwert");
$mc2 = new MyClass2();

Wenn der Bauplan einer Variablen erstellt wird, wenn wir also die Klasse MyClass definieren, dann wissen wir noch nicht, unter welchen Namen die Slots und Instanzfunktionen des Objektes einmal angesprochen werden. Daher können wir in der Funktion nicht $o->a oder $p->c() schreiben, sondern müssen irgendetwas allgemeineres als Platzhalter notieren.

Die Pseudovariable $this steht dabei in einer Instanzfunktion als Platzhalter für den tatsächlichen Namen dieses Objektes. Innerhalb des Objektes $o steht $this->a also als Platzhalter fuer $o->a, innerhalb des Objektes $p steht $this->a jedoch als Platzhalter fuer $p->a. Man kann $this auch als "meine" lesen, also ist $this->a "mein Slot a" und $this->c() "meine Instanzfunktion c".

Referenzen in PHP

In PHP ist es so, dass jede Zuweisung eine Kopie der angesprochenen Variablen erzeugt:

$a = 10;
$b = 20;
$c = 30;

$arr = array( $a, $b, $c );

$arr[1] = 22;

Dies verändert den Wert von $arr[1] auf 22, aber der Wert von $b ändert sich nicht. Dies gilt auch bei Funktionsaufrufen:

function m($p1) {
  $p1 = 22;
}

m($b);

Dies setzt den Wert von $p1 auf 22, aber $p1 ist eine Kopie von $b und $b verändert den Wert nicht. Da beim Aufrufen ("Call") einer Funktion der Wert von $b in $p1 kopiert, also der Wert von $b an $p1 übergeben wird, nennt man dies "Call by value".

Man könnte auch eine Funktion schreiben, die den Namen der Variablen übergeben bekommt und dann mit Hilfe dieses Namens auf die globale Variable dieses Namens zugreift:

function m2($p1) {
  $GLOBALS[$p1] = 22;
}

m2("b");

Da in diesem Fall der Name der Variablen übergeben wird und mit Hilfe dieses Namens dann die originale Variable dieses Namens verändert wird, nennt man diese Art des Funktionsaufrufes "Call by name" (obwohl bei richtigem Call by Name noch viel obskurere Dinge möglich sind)..

Schon in PHP3 ist es so, dass man eine Funktion als "Call by Reference" definieren kann. In diesem Fall wird keine Kopie des Variablenwertes übergeben, sondern der Formalparameter p1 wird zu einem alternativen Namen für den übergebenen Wert. Wieder wird der Wert der originalen globalen Variablen verändert:

function m3(&$p1) {
  $p1 = 22;
}

m3($b);

Hier ist $p1 in m3() ein alternativer Name fuer die Variable, die beim Aufruf genennt wird. In m3($b) wird die Variable $b benannt, also ist $p1 in diesem Aufruf ein alternativer Name fuer $b. Immer wenn innerhalb dieses Aufrufes von m3() $p1 verwendet wird, wird $b angesprochen.

Seit PHP4 ist es nun so, dass man Referenzen auch an anderer Stelle verwenden kann, zum Beispiel in der o.a. Arraydefinition:

$a = 10;
$b = 20;
$c = 30;

$arr = array( &$a, &$b, &$c );

$arr[1] = 22;

print $b;

Dieses Beispiel druckt den Wert 22. In diesem Beispiel sind $arr[0], $arr[1] und $arr[2] keine Kopien der Werte von $a, $b und $c, sondern die $arr-Variablen werden zu alternativen Namen fuer $a, $b und $c. Jeder Zugriff auf $arr[1] spricht also in Wirklichkeit $b an.

Man liest "$arr[1] = &$b" als "$arr von Eins ist eine Referenz auf $b".

Klassenfunktionen in PHP4

Wenn man eine Klasse definiert, kann man in PHP3 mit dieser Klasse nichts anderes machen als mit Hilfe von "new" Objekte dieser Klasse zu erzeugen.

In PHP4 kann man jedoch nicht nur Funktionen eines Objektes aufrufen, sondern auch schon Funktionen einer Klasse. Das sind also nichts anderes als gewöhnliche Funktionsaufrufe, nur dass die Funktionsnamen sehr seltsam aussehen:

class MyClass3 {
  function c() {
    print "Ich bin Funktion c() in der Klasse MyClass\n";
  }
}

MyClass3::c();

Dieses Stück Code haette man so auch schreiben koennen und es wäre leichter zu verstehen gewesen:

function c() {
  print "Ich bin die stinknormale Funktion c()\n";
}

c();

Man kann dies verwenden, um in abgeleiteten Klassen Funktionen gleichen Namens in Oberklassen aufzurufen.

6.2. Welche Datentypen gibt es in PHP?

Keywords: Typ | Variable | Konstante | Array

Antwort von Kristian Köhntopp

Dadurch, dass PHP allen Variablenbenutzungen das Markierungszeichen $ (Dollar) voranstellt, ist es möglich, Variablen in Stringkonstanten zu interpolieren, wie man es von der Unix-Shell her kennt. Will man das Markierungszeichen selbst ausgeben, kann man ihm es durch einen vorangestellten \ (Backslash) entwerten. Einen Backslash gibt man aus, indem man ihn entwertet: \\ (Backslash Backslash).

In PHP gibt es die folgenden Datentypen:

Skalare Werte: boolean, integer, float, string

# Zuweisung
$mybool  = true;
$myint   = 1;
$myfloat = 3.14;
$mystring= "hallo";

# Verwendung
$yourbool  = not $mybool;
$yourint   = $myint * 2;
$yourfloat = $myfloat * 2.71828;
$yourstring= $mystring . " du da!";

# Ausgabe
print "$yourbool\n";
print "$yourint\n";
print "$yourfloat\n";
print "$yourstring\n";

Felder (Arrays)

Der Gebrauch von Feldern ist im Kapitel Arrays und Arrayvariablen ausführlicher erläutert.

PHP unterscheidet nicht zwischen Feldern (Arrays) mit Integer-Index und Hashes (Assoziativen Arrays) mit beliebigen Indices.

# Arrays

# Zuweisung
$a1    = array( 10, 20, 30);
$a2[0] = 10;
$a2[2] = 30;
$a2[1] = 20;

# Verwendung
$a3[0] = $a1[0] + $a2[2]; // $a3[0] ist 40
$a3[]  = $a1[1] + $a1[0]; // $a3[1] ist 30, Index autom. vergeben

# Ausgabe
printf("A1: %s %s %s\n", $a1[0], $a1[1], $a1[2]);

# Aufzählung
for ($i=0; $i<=2; $i++) {
  printf("%s: %s\n", $i, $a1[$i]);
}
# Hashes

# Zuweisung
$a1    = array( "peter" => 10, "paul" => 20, "mary" => 30);
$a2["peter"] = 10;
$a2["mary"]  = 30;
$a2["paul"]  = 20;

# Verwendung
$a3["peter"] = $a1["peter"] + $a2["mary"]; // $a3["peter"] ist 40

# Ausgabe
printf("A1: %s %s %s\n", $a1["peter"], $a1["paul"], $a1["mary"]);

# Aufzählung
foreach($a1 as $k => $v) {
  print("%s: %s\n", $k, $v);
}

Objekte

Der Gebrauch von Objekten ist im Kapitel Klassen und Objekte ausführlicher erläutert.

Spezielle Typen: Resource und NULL

Eine Resource ist eine spezielle Variable, die eine Referenz auf eine externe Resource enthält. Resourcen werden von bestimmten Funktionen erzeugt und benutzt. Wenn man zum Beispiel mit $db = mysql_connect(...) eine Datenbankverbindung herstellt, dann ist $db vom Typ Resource.

Der spezielle NULL Wert steht dafür, dass eine Varaiable keinen Wert hat. NULL ist der einzig mögliche Wert des Typs NULL.

Eine Variable wird als NULL interpretiert, wenn

  • ihr die Konstannte NULL als Wert zugewiesen wurde.

  • ihr bis jetzt kein Wert zugewiesen wurde.

  • sie mit unset() gelöscht wurde.

6.3. Was muss ich bei der automatischen Typ-Konvertierung beachten?

Antwort von Kristian Köhntopp

PHP konvertiert Typen für manche Funktionen, ohne dass der Programmierer darauf Einfluss nehmen kann. Hier ein Beispiel, wie der Stringvergleich mit strcmp() intern funktioniert:

  wenn is_numeric($a) und is_numeric($b),
  dann vergleiche $a und $b als Zahlen, auch wenn die Variablen
  den Typ String haben (und kein Cast der Welt kann das verhindern),
    dabei verwende BC-Math wenn notwendig und vorhanden,
    sonst float
    sonst integer
  sonst
    mache einen Stringvergleich wie jedermann erwartet haette.

Also ist

kk@kris:~/Source/php4/Zend> php
<?php
  $a = "2";
  $b = "10";
  echo "a ist ".gettype($a)."\n";
  echo "b ist ".gettype($b)."\n";
  if ($a > $b)
    echo "stringvergleich\n";
  else
    echo "numerischer vergleich\n";
?>
a ist string
b ist string
numerischer vergleich

Das ist nicht zu ändern. $a und $b sind Strings und werden als Zahlen verglichen, weil sie wie welche aussehen, und man kann es nicht verhindern, noch kann man die Zahldarstellung wählen, in der verglichen wird. Will man einen Stringvergleich erzwingen, muss man schreiben:

kk@kris:~/Source/php4/Zend> php
<?php
  $a = "2";
  $b = "10";
  echo "a ist ".gettype($a)."\n";
  echo "b ist ".gettype($b)."\n";
  if ("x$a" > "x$b")
    echo "stringvergleich\n";
  else
    echo "numerischer vergleich\n";
?>
a ist string
b ist string
stringvergleich

Hier bewirken die führenden Buchstaben x, dass die Konvertierung in Zahldarstellung fehlschlägt und ein Stringvergleich wird erzwungen.

6.4. Wie schreibe ich eine Funktion mit einer variablen Anzahl von Argumenten?

Keywords: Argument | Funktion | Array

Antwort von Kristian Köhntopp

In PHP kann man Funktionsparameter mit Default-Werten versehen. Läßt man die Argumente der Funktion von hinten nach vorne weg, werden stattdessen die Defaults eingesetzt. Defaultwerte müssen skalare Konstanten sein. Variable Ausdrücke (Variablen, Funktionsaufrufe) oder nichtskalare Werte (Arrays, Objekte) sind nicht gestattet.

function beispiel($p = "default") {
  printf("Der Parameter p hat den Wert %s\n", $p);
}

beispiel("hallo");
beispiel();

Auf diese Weise kann man jedoch keine echten variadischen Funktionen schreiben. So ist es zum Beispiel nicht möglich eine Funktion wie printf() in PHP3 zu schreiben. Man kann variadische Funktionen jedoch durch die Übergabe eines Array- oder Hashparameters simulieren.

function beispiel2($p) {
  if (!isset($p) or !is_array($p))
    # Defaults setzen
    $p = array("para1" => "bla", "para2" => "fasel");

  if ($p["para1"])
    machdies();

  if ($p["para2"])
    machdas();
}

beispiel2(array("para1" => "laber", "para2" => "lall"));

Seit PHP4 sind auch echte variadische Funktionen möglich. Dort gibt es die drei Funktionen

func_num_args()

Diese Funktion liefert die Anzahl der Funktionsargumente als Integer.

func_get_arg()

Diese Funktion bekommt eine Argumentnummer als Parameter und liefert des Wert des Funktionsargumentes mit diesem Index zurück.

func_get_args()

Diese Funktion liefert alle Argumente einer Funktion als Array zurück.

Eine echte variadische Funktion kann also in PHP4 folgendermaßen geschrieben werden:

function beispiel3() {
    $args = func_get_args();
    for($i=0; $i<count($args); $i++) {
         print($args[$i]."\n");
    }
}

beispiel3("das", "ist", "ein", "test");

6.5. Wie gebe ich mehrere Werte mit einer Funktion zurück?

Antwort von Kristian Köhntopp

PHP kann nur einen Wert mit return() zurückgeben.

Wie bei der Übergabe von Funktionsargumenten, kann man aber auch hier beliebige Werte in einem Array zusammenfassen und so eine Rückgabe mehrerer Werte simulieren:

function beispiel() {
    $ret = array(1, 2, 3);

    return($ret);
}

Im Funktionsaufruf kann man list() verwenden, um Variablen die Elemente des zurückgelieferten Arrays zuzuweisen:

list($var1, $var2, $var3) = beispiel();

Wenn man nicht tatsächlich mehrere Werte zurückgeben möchte, sondern lediglich mehrere Werte in einem Funktionsaufruf beeinflussen möchte, dann kann stattdessen auch mit Referenzparametern arbeiten.

6.6. Wie schreibe ich ein Script, das beliebige Parameter verarbeitet?

Keywords: Argument | GET | POST

Antwort von Kristian Köhntopp

Manchmal möchte man ein Script zu schreiben, das beliebige Parameter verarbeitet - etwa ein Script, das alle gegebenen Parameter in eine Mail verpackt und diese dann versendet.

Vollständiges Beispiel:

<?php
  $msg = "";

  $msg .= "Alle GET-Parameter\n";
  $msg .= "==================\n";
  $msg .= "\n";
  foreach ($_GET as $k => $v) {
    $msg .= sprintf("%s\n  %s\n\n", $k, $v);
  }

  $msg .= "Alle POST-Parameter\n";
  $msg .= "===================\n";
  $msg .= "\n";
  foreach ($_POST as $k => $v) {
    $msg .= sprintf("%s\n  %s\n\n", $k, $v);
  }

  // Hier kann $msg per Mail versendet werden.
?>

Will man einfach nur alle Parameter haben, kann man statt $_GET und $_POST auch einfach $_REQUEST verwenden.

6.7. Variable Variablen

Keywords: Variable | Array | Global

Antwort von Kristian Köhntopp

Manchmal möchte man auf Variablen zugreifen, deren Namen variabel sind. Zum Beispiel könnte man die Variablen mit den Namen $myvar1, $myvar2, $myvar3, ..., $myvar9 haben.

Am günstigsten wäre es, in so einem Fall ein Array zu nehmen.

for ($i=0; $i<10; $i++)
	echo $myvar[$i];

Wenn es unbedingt skalare Variablen sein müssen, kann man stattdessen über das $GLOBALS[]-Array zugreifen:

for ($i=0; $i<10; $i++) {
  if (isset($GLOBALS["myvar$i"]))
    printf("Variable var%d existiert und ihr Wert ist %s<br />\n",
      $i, $GLOBALS["myvar$i"]);
}

Statt echo $GLOBALS[$lall]; kann man auch die zwei Befehle global $$lall; echo $$lall; verwenden. Empfohlen ist jedoch das Konstrukt mit $GLOBALS[], weil es leichter zu lesen und zu verstehen ist. Das gilt besonders bei Dateinamen, die sich aus einem konstanten Stamm und einem variablen Anteil zusammensetzen.

Eine weitere alternative Schreibweise für variable Variablen ist ${$lall}; für zusammengesetzte Variablennamen entsprechend beispielsweise ${"datei_$lall"}.

6.8. Was ist der Unterschied zwischen isset() und einem Vergleich auf den Leerstring?

Keywords: Variable | Typ

Antwort von Georg Maaß

if($var) evaluiert nur dann zu true, wenn $var keinen der folgenden Werte darstellt: false, 0, 0.0, "" oder "0", NULL, array() oder new stdClass. Alle diese Werte bedeuten false in ihrem jeweiligen Typ (Bool, Integer, Float, String, Null, Array, Object).

if(isset($weiter)) evaluiert immer zu true, wenn $weiter nicht undefined ist.

Im ersten Fall wird der Inhalt, im zweiten Fall die Existenz der Variablen bewertet.

Ein ähnliches Problem tritt bei Vergleichen auf: if($var == false) evaluiert immer dann zu true, wenn $var einen der obigen Werte darstellt. PHP führt hier eine automatische Typenkonvertierung durch, wodurch die beiden Variablen als äquivalent angesehen werden.

Diese Typkonvertierung kann mit den Vergleichsoperatoren === (drei Gleichheitszeichen) und !== vermieden werden. Der Vergleich === ist nur dann wahr, wenn die beiden Operanden den gleichen Typ und den gleichen Wert haben.

6.9. Wie kann ich JavaScript-Funktionen aus PHP heraus aufrufen?

Keywords: JavaScript | Funktion

Antwort von Johannes Frömter

JavaScript läuft auf dem Client (im Browser), PHP läuft auf dem Server, also genau am anderen Ende der Welt; wenn die HTML-Seite beim Browser ankommt, ist PHP mit der Arbeit schon fertig. Der Aufruf einer JavaScript-Funktion aus PHP ist also prinzipiell unmöglich.

Allerdings kann man Werte von PHP an JavaScript übergeben; um eine in PHP vorhandene Variable in JavaScript verwenden zu können, muss man sie innerhalb eines <script>-Bereiches ausgeben:

<script type="text/javascript" language="JavaScript">
<?php
printf("js_var = '%s';\n",
        strtr(addslashes($php_var),
        array("\r" => '\r', "\n" => '\n'))
      );
?>
</script>

Auf diese Weise wird die JavaScript-Variable js_var mit dem Wert der PHP-Variable $php_var vorbelegt. Natürlich kann man so auch beliebigen ausführbaren JavaScript-Code ausgeben, den der Browser anschließend verarbeitet. Wichtig ist nur zu verstehen, dass ein logischer, räumlicher und auch zeitlicher Schnitt zwischen PHP- und JavaScript-Code vorhanden ist.

6.10. Wie kann ich PHP-Funktionen aus JavaScript heraus aufrufen?

Keywords: JavaScript | Funktion | GET | POST

Antwort von Johannes Frömter

Da JavaScript auf dem Client und PHP auf dem Server läuft, kann man aus JavaScript auch keine PHP-Funktionen direkt aufrufen. PHP wird immer als das Resultat eines HTTP-Requests ausgeführt, also beim Holen einer Seite mit GET oder beim Verarbeiten eines Formulares mit POST. Es ist also nicht möglich, aus JavaScript heraus eine PHP-Funktion aufzurufen, außer durch Erzeugen eines HTTP-Requests (durch den von PHP eine neue Seite generiert wird).

Einen GET-Request mit JavaScript erreicht man prinzipiell durch

<script type="text/javascript" language="JavaScript">
window.location.href = "script.php?php_var=" + escape(js_var);
</script>

Im dadurch aufgerufenen PHP-Skript script.php ist dann die Variable $_REQUEST['php_var'] mit dem Wert von js_var verfügbar. Einen POST- oder GET-Request mit einem Formular erreicht man durch

<script type="text/javascript" language="JavaScript">
document.formularname.submit();
</script>

Weitere Informationen zur Variablenübergabe: siehe Variablen und Formulare in dieser FAQ.

6.11. Wie heißt die Variable, die ich suche?

Keywords: Variable | Konstante | Name

Antwort von Johannes Frömter

Die Antwort auf diese Frage lautet ausnahmsweise nicht 42, sondern phpinfo() . In der Ausgabe dieses Befehls finden sich neben Konfigurationsangaben sehr viele vordefinierte Variablen und deren Namen, einschließlich Umgebungsvariablen, übergebenen Parametern, Cookie-Werten etc. pp. (im unteren Drittel der Ausgabe).

Da sich die Namen der vordefinierten Variablen im Laufe der Entwicklung von PHP mehrmals geändert haben, und ihr Vorhandensein teilweise auch von der Umgebung abhängen kann (Webserver, Aufruf per Kommandozeile usw.), ist es immer ratsam, einen Blick in die Ausgabe von phpinfo() zu werfen. Man lege also eine Datei mit folgendem Inhalt an und rufe sie per Browser auf:

<?php
    phpinfo();
?>

Gerade auch bei Problemen mit Formular-Variablen oder Cookies kann man in das Script den phpinfo()-Aufruf einbauen, um einen Einblick in die zur Verfügung stehenden Daten zu bekommen.

7. Stringfunktionen

7.1. Was ist besser, print() oder echo?

Keywords: Unterschied | Vergleich

Antwort von Kristian Köhntopp

echo() ist ein internes Sprachkonstrukt und gibt nichts zurück, print() ist eine Expression. Man kann print() also in Situationen benutzen, wo Expressions gefragt sind, z. B. $var ? print("...") : null;.

echo hat eine variable Argumentliste, dabei muss man aber auf die Klammern verzichten: echo $var1, $var2;. print() kann nur ein Argument haben und gibt immer 1 zurück.

7.2. Wie zerlege ich einen String?

Antwort von Kristian Köhntopp

Man kann die einzelnen Zeichen in einem String ähnlich wie ein Array ansprechen, allerdings verwendet man anstelle der eckigen Klammern [ und ] geschweifte Klammern: { und }.

$str = "teststring";
$len = strlen($str);
for($i=0; $i<$len; $i++)
  printf("Zeichen %d ist %s<br>\n", $i, $str{$i});

Diese Methode ist auch geeignet, um einen String in ein Array umzuwandeln:

function stringToArray($string) {
  $array = array();
  for($i=0; $i<strlen($string); $i++)
    $array[$i] = $string{$i};
  return $array;
}

Mit Hilfe der Funktion substr() kann man Teilstrings aus einem String herausschneiden.

Mit Hilfe der Funktion explode() kann man einen String an einem Trennzeichen in ein Array zerlegen.

  $str = "dies ist ein teststring.";
  $avar = explode(" ", $str);
  $len = count($avar);
  for ($i=0; $i<$len; $i++)
    printf("%d: %s<br>\n", $i, $avar[$i]);

Dieses Beispiel zerlegt den gegebenen Teststring an den Leerzeichen und erzeugt ein Array $avar mit den Indices 0 bis 3 (4 Elementen).

Kompliziertere Zerlegungen lassen sich mit Hilfe der Funktion preg_split() vornehmen. Das veraltete, weniger leistungsfähigere und langsamere split() könnte man auch verwenden.

  $str = "ich bin  ein    sehr komplizierter test, nicht wahr?";
  $avar = preg_split("/[ \t.!?]+/", $str);
  $len = count($avar);
  for ($i=0; $i<$len; $i++)
    printf("%d: %s<br>\n", $i, $avar[$i]);

Im Gegensatz zum vorhergehenden Beispiel werden hier mehrfache Leerzeichen nicht als mehrfache Trennungen gezählt und auch Satzzeichen werden zu den Trennzeichen gezählt.

7.3. Wie zerlege ich eine URL?

Keywords: parsen | Hyperlink

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion parse_url() kann eine URL in ihre Bestandteile zerlegt werden.

  $str = "http://user:password@www.koehntopp.de:80/kris/artikel#php";
  $avar = parse_url($str);
  reset($avar);
  while(list($k, $v) = each($avar))
    printf("k=%s, v=%s<br>\n", $k, $v);

Ein QUERY_STRING kann mit Hilfe der Funktion parse_str() in seine Variablen zerlegt werden.

7.4. Wie gebe ich eine Zahl formatiert aus?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion number_format() oder mit Hilfe von printf() .

7.5. Wie kann ich Zeilenumbrüche verarbeiten?

Keywords: String | Zeilenumbruch | Zeile | Enter

Antwort von Johannes Frömter

Die sogenannten "Zeilenumbrüche" sind im Prinzip ganz gewöhnliche Bytes wie ein A oder % auch - sie werden erst bei der Anzeige in einem Editor als Zeilenumbrüche dargestellt. Hässlicherweise haben sich für diesen Zweck unterschiedliche Zeichen (Bytes) etabliert: Windows verwendet \r\n, Unix \n und der Mac \r als "neue-Zeile-Zeichen". Dabei gilt:

Escape-  Hex-  ASCII-  Abkür-  Name/Bedeutung      Name/Bedeutung
Sequenz  Code  Code    zung    englisch            deutsch
-------  ----  ------  ------  ------------------  --------------
\r       0D    13      CR      carriage return     Wagenrücklauf
\n       0A    10      LF      line feed, newline  Zeilenvorschub
-------  ----  ------  ------  ------------------  --------------

Die Unterscheidung zwischen Wagenrücklauf und Zeilenvorschub rührt von den Zeilendruckern her, bei denen das zwei separate Steuersignale sind.

Die Escape-Sequenzen können in PHP direkt in Strings, die zwischen " (Anführungszeichen, double quotes) stehen, sowie in Regulären Ausdrücken verwendet werden. Ebenso kann man dort \x0D, \x0A etc. (hexadezimale Escape-Sequenzen) verwenden. Die ASCII-Codes kann man von der Funktion chr() umwandeln lassen.

Empfängt man Daten von unbekannten Clients, sollten verarbeitende Funktionen mit allen Varianten zurecht kommen; eine universelle Funktion zum Ersetzen von Zeilenumbrüchen durch Leerzeichen sieht z. B. so aus:

$string = preg_replace('/\r\n|\r|\n/', ' ', $string);

7.6. Wie kann ich Zeilenumbrüche in <br> umwandeln?

Keywords: Zeilenumbruch | Zeile | HTML

Antwort von Kristian Köhntopp

PHP bietet die Funktion nl2br() , die vor jedem Zeilenumbruch ein <br> einfügt. Ab PHP 4.0.5 wird statt <br> das XHTML-konforme <br /> verwendet. Diese Schreibweise ist auch mit älteren Browsern kompatibel und wird auf jeden Fall empfohlen. Anzumerken ist hierbei, dass die von Mac als "Neue-Zeile-Zeichen" verwendeten Carriage-Returns (\r) nicht als solches erkannt werden und deshalb nicht in <br /> umgewandelt werden.

  // Einlesen der Datei "datei" in den String $str
  $str = implode("", @file("datei"));
  // Ausgeben der Datei mit Umbrüchen
  print nl2br($str);

Sollen Zeilenumbrüche komplett ersetzt werden, benutzt man z. B. str_replace() :

$string = str_replace("\n", "<br>", $string);

Zum Thema "Zeilenumbruch" siehe auch: "Wie kann ich Zeilenumbrüche verarbeiten?".

Antwort von Johannes Frömter

Sollte man in die Verlegenheit kommen, eine Umkehrfunktion zu nl2br() zu benötigen, muss man einen Regulären Ausdruck bemühen.

function br2nl($str) {
    return preg_replace("=<br(>|([\s/][^>]*)>)\r?\n?=i", "\n", $str);
}

Der Ausdruck ist etwas länglich, weil er sicherstellt, dass wirklich nur <br>-Tags (die allerdings in allen Variationen!), nicht aber XML-Tags o. ä. (<brown> z.B.) umgewandelt werden - sicher ist sicher...

7.7. Wie breche ich einen String nach x Zeichen um?

Antwort von Johannes Frömter

Ab PHP 4.0.2 gibt es die Funktion wordwrap() , um lange Strings auf eine definierte Zeilenlänge zu bringen. Als Default wird nach 75 Zeichen mit \n umgebrochen, man kann aber optional als dritten bzw. vierten Parameter auch eigene Werte angeben, was gerade bei der Ausgabe in einer HTML-Seite praktisch ist:

echo wordwrap($ganzLangerText, 25, "<br>", 1);

Der vierte Parameter cut ist in PHP 4.0.3 hinzugekommen, er bewirkt, dass der String auf jeden Fall (auch mitten in einem Wort) umgebrochen wird. Hinweis: der Parameter cut sollte möglichst immer gesetzt werden, es gab einige PHP-Versionen, in denen wordwrap() bei Wörtern mit einer Länge > width sonst in einer Endlosschleife landet.

Kann man wordwrap() nicht benutzen, helfen frei verfügbare Scripte wie z.B. textwrap von Brian Moon.

7.8. Wie kann ich einen String kürzen?

Antwort von Johannes Frömter

Im einfachsten Fall erreicht man dies mit der "Allround-Funktion" substr() (weitere Beispiele im Manual):

// Die ersten 20 Zeichen von $string ausgeben:
echo substr($string, 0, 20);

Soll die Ausgabe nicht mitten in einem Wort unterbrochen werden, bietet es sich an, die Funktion wordwrap() zu "missbrauchen":

// Nur ganze Wörter, max. 20 Zeichen von $string ausgeben:
$parts= explode("\n", wordwrap($string, 20, "\n"));
echo $parts[0];

Hier wird an der gewünschten Stelle ein Zeilenumbruch durch wordwrap() eingefügt, anschliessend der String an den Zeilenumbrüchen in Arrays von Strings zerlegt und schliesslich das erste Array-Element ausgegeben.

7.9. Wie kann ich einen String als PHP-Code ausführen?

Keywords: String | Code | Datenbank | Datei | ausfuehren

Antwort von Johannes Frömter

Mit der Funktion eval() kann man PHP-Code, der in einer Variablen als String vorliegt, ausführen lassen. eval() schaltet direkt in den PHP-Modus, d. h. PI-Tags (processing instructions, üblicherweise <?php und ?>) müssen im String nicht enthalten sein. Der String darf aber PIs enthalten, um vom PHP- in den HTML-Modus (und zurück) zu wechseln; beginnt der String mit HTML, so ist zuerst ein schließender PI-Tag nötig:

$html_php_mix = '<h1> <?php echo "GROSS"; ?> </h1>';
eval("?> $html_php_mix <?php ");

Der öffnende PI-Tag zum Schluss ist nicht unbedingt notwendig - wenn man ihn schreibt, dann allerdings ist das Leerzeichen danach wichtig. Alle PHP-Anweisungen müssen korrekt mit einem Semikolon beendet werden (in normalem PHP-Code darf das Semikolon nach der letzten Anweisung fehlen).

Achtung: Der Funktion eval() darf man nur absolut vertrauenswürdigen Code übergeben; Code aus unbekannter Quelle kann äußerst gefährliche Befehle wie z.B. unlink($_SERVER['PHP_SELF']), mysql_drop_db() oder system() enthalten und großen Schaden anrichten.

8. Reguläre Ausdrücke

8.1. Wie kann ich mehr über Reguläre Ausdrücke lernen?

Keywords: Tutorial | Buch

Antwort von Kristian Köhntopp

Bei O'Reilly gibt es das ausgezeichnete Buch Mastering Regular Expressions von Jeffrey E. F. Friedl, welches auch als deutsche Übersetzung unter dem Titel Reguläre Ausrücke vertrieben wird. Es enthält eine ausgezeichnete Übersicht über die verschiedenen Formen von regulären Ausdrücken in Unix mit Beispielen.

Online gibt es z.B. das Tutorial Rx (englisch) über Reguläre Ausdrücke nach POSIX-Standard (das sind die ereg()-Funktionen in PHP).

Die preg()-Funktionen von PHP sind praktisch 100% kompatibel zu den Regulären Ausdrücken in Perl - die ausführenden Funktionen heißen allerdings anders; insofern eignet sich jede Anleitung zu Perl Regular Expressions, wenn es nur um die Bedeutung der einzelnen Zeichen geht.

8.2. Soll ich ereg() oder preg() verwenden?

Keywords: POSIX | PCRE | ereg()

Antwort von Kristian Köhntopp

Wenn die verwendete Version von PHP ausreichend neu ist und das Modul PCRE aktiviert ist (dies kann man mit einem Aufruf von phpinfo() leicht feststellen), dann sollte man wo immer es geht die preg-Funktionen verwenden. Sie sind nicht nur schneller, sondern auch flexibler und leistungsfähiger als die alten ereg-Funktionen.

Es gibt keinen Grund mehr, die ereg-Funktionen noch zu verwenden außer Rücksicht auf veraltete Installationen.

8.3. Wie verwende ich die preg()-Funktionen?

Keywords: PCRE

Antwort von Kristian Köhntopp

Die preg()-Funktionen sind im Abschnitt Perl-compatible regular expressions des Online-Handbuches beschrieben. Es handelt sich um die Funktionen

Für alle Funktionen gilt das in den Abschnitten Pattern Modifiers und Pattern Syntax gesagte.

8.4. Was sind Reguläre Ausdrücke?

Keywords: Definition | Grundlagen

Antwort von Kristian Köhntopp

Reguläre Ausdrücke sind Suchmuster, die sich auf Strings anwenden lassen und für die entscheidbar ist, ob sie auf den String passen (match) oder nicht passen. So paßt das Suchmuster ei auf den String Weichei, weil darin die Zeichenfolge ei enthalten ist, aber nicht auf den String Warmduscher. Wendet man ein Suchmuster auf eine Menge von Strings an, dann bekommt man zwei Teilmengen, nämlich die Menge aller Strings, auf die das Muster paßt und die Menge aller Strings, auf die das Muster nicht paßt. Meistens interessiert man sich für eine der beiden Teilmengen ("Finde alle Namen, die mit einem einem A beginnen.", "Finde alle Zeichen, die nicht rechts von einem Kommentarzeichen stehen.")

Reguläre Ausdrücke werden meistens durch einen endlichen Automaten realisiert. Die Informatik kennt Verfahren, mit denen man automatisch einen Automaten generieren kann, der für ein bestimmtes Suchmuster entscheidet, ob es auf einen String paßt oder nicht. Auch die regulären Ausdrücke in PHP funktionieren so: Bei der ersten Benutzung eines regulären Ausdruckes wird ein solcher Automat intern generiert (das Muster wird "compiliert") und dann angewendet. Bei späteren Benutzungen desselben Suchmusters kann dieser Automat dann unter Umständen wieder verwendet werden, was deutlich schneller ist.

Einige Suchmuster und Bedingungen sind zu komplex, als dass man sie mit Hilfe von regulären Ausdrücken und Automaten formulieren kann. Typische Beispiele dafür sind Dinge, die Abzählungen erforderlich machen ("Finde alle Worte, die aus genausovielen b's bestehen, wie sie a's enthalten") und Dinge, die Vorbedingungen notwendig machen ("Finde alle Worte print, aber nur, wenn sie nicht in Anführungszeichen stehen oder Bestandteil eines Kommentares sind."). In diesem Fällen braucht man leistungsfähigere Konzepte und Werkzeuge, kontextfreie oder kontextsensitive Grammatiken und dazu passende Parser.

In der Praxis verwendet man reguläre Ausdrücke, um zu entscheiden, ob ein String bestimmten formalen Kritierien genügt ("Akzeptiere den Formularwert nur dann, wenn er ausschließlichlich Ziffern enthält.") oder um bestimmte Teilstücke aus Strings herauszuschneiden ("Liefere mit den Text zwischen dem begin und end aus dem gegebenen String.").

8.5. Welche Bauelemente kommen in Regulären Ausdrücken vor?

Antwort von Kristian Köhntopp

Suchmuster in regulären Ausdrücken bestehen aus gewöhnlichen Zeichen und Zeichen mit einer Sonderbedeutung. Gewöhnliche Zeichen in Suchausdrücken stehen für die entsprechenden Zeichen in einem String. Der Suchausdruck hallo paßt also auf alle Strings, die irgendwo genau diese Zeichenfolge enthalten. Zeichen mit Sonderbedeutung stehen als Platzhalter für ein oder mehrere andere Zeichen, für Zeilenanfänge oder -enden oder für andere Sonderfunktionen. Sie sind es, die reguläre Ausdrücke eigentlich mächtig und sinnvoll machen.

In regulären Ausdrücken gibt es Zeichenmengen (wird weiter unten erklärt), die durch eckige Klammern [ und ] eingeschlossen werden. Bei Zeichen mit Sonderbedeutung gelten leicht unterschiedliche Regeln, je nachdem ob man gerade eine außerhalb einer Zeichenmenge arbeitet oder innerhalb.

Außerhalb von Zeichenmengen gibt es die folgenden besonderen Regeln:

  • Der Backslash \ wird als Escape-Zeichen verwendet, mit unterschiedlichen Anwendungen:

    • Für Sonderzeichen (also alle nicht-alphanumerischen Zeichen) nimmt es den Zeichen ihre besondere Bedeutung, sodass man sie "wörtlich" in das Suchmuster einfügen kann. Das Suchmuster \* paßt also auf genau ein Sternchen.

    • Für alphabethische Zeichen hat es eine besondere Bedeutung ähnlich wie in der Programmiersprache C: \r steht für ein Return (ASCII 13), \n steht für ein Linefeed (ASCII 10) und so weiter. Die Zeichenfolgen \d, \D, \w, \W, \s und \S beschreiben bestimmte vordefinierte Zeichenmengen (siehe unten).

    • In Zeichenfolgen \xYY definiert es das Zeichen mit dem hexadezimalen Zeichencode YY. Ein \x0A steht also für ein Zeichen mit einem Zeichencode ASCII 10, Linefeed.

    • In Zeichenfolgen \YYY definiert es das Zeichen mit dem oktalen Zeichencode YYY. Ein \012 steht also für ein Zeichen mit dem Zeichencode ASCII 10, Linefeed. Diese Schreibweise ist jedoch nicht empfohlen, weil sie sich mit einer anderen Schreibweise für sogenannte Backreferences (siehe unten) überlappt.

  • Das Dachzeichen ^ steht als Markierung für einen Stringanfang: Das Suchmuster ^hallo paßt auf alle Strings, die mit dem Wort hallo beginnen, jedoch nicht auf Strings, die hallo irgendwo in der Mitte enthalten.

  • Entsprechend steht das Stringzeichen $ für ein Stringende: Das Suchmuster Welt!$ paßt auf Zeichenketten, die mit dem Text Welt! enden. Das Suchmuster ^$ paßt auf Strings, die nur aus einem Beginn und einem Ende bestehen, also auf Leerstrings.

  • Der Punkt . paßt auf genau ein beliebiges Zeichen außer dem Zeilenendezeichen \n. Das Suchmuster ... paßt also auf alle Strings, die aus genau drei Zeichen bestehen.

  • Die öffnende eckige Klammer [ leitet eine Zeichenmenge ein.

  • Das Pipelinezeichen | kennzeichnet einen Zweig einer Alternative. So paßt das Suchmuster (Sie|Du), sobald die Zeichenfolge Sie oder die Zeichenfolge Du im Text enthalten ist.

  • Die runden Klammern () schließen ein Untermuster bzw. Teilmuster ein. Man kann dies verwenden, um einen Teilbereich eines Suchmusters zum Zwecke der späteren Verwendung zu markieren oder um Teilmuster zu gruppieren.

    Für die Gruppierung findet man im vorhergehenden Punkt schon ein Beispiel: (Sie|Du) definiert ein Muster, das auf Sie oder Du paßt. Dagegen definiert Si(e|D)u ein Muster, das entweder auf Sieu oder auf SiDu paßt.

    Markierte Teilbereiche eines Suchmusters, sogenannte Backreferences lassen sich später nach der Anwendung des Musters extrahieren und weiterverarbeiten: Das Muster "(.)" paßt auf genau ein beliebiges Zeichen, das zwischen Anführungszeichen steht. Welcher Buchstabe aus dem getesteten String genau gepaßt hat, kann man nach der Anwendung des Suchmusters erfragen. Dies verwendet man oft, um Teilstrings mit einem bestimmten Aussehen aus einem größeren String herauszuschneiden.

    Innerhalb eines regulären Ausdrucks werden die öffnenden runden Klammern von links nach rechts durchnummeriert. Innerhalb desselben Ausdrucks kann man sich mit Backreferences auf den Text beziehen, der von einer Klammer markiert worden ist: \1 bezeichnet den Text, der von der ersten öffnenden Klammer umschlossen wird, \2 den Text in der zweiten Klammer und so weiter. Dazu in einem der folgenden Punkte mehr.

  • Mit Hilfe von Quantifizierern in geschweiften Klammern {} kann man festlegen, wieviele Wiederholungen des vorhergehenden Ausdrucks gesucht werden: {n,m} steht für n bis m Wiederholungen des vorhergehenden Ausdrucks, {n,} für mindestens n Wiederholungen, {,m} für maximal m Wiederholungen. Mit Hilfe von {n} bezeichnet man genau n Wiederholungen.

    So paßt das Suchmuster a{3,5} auf alle Strings, die zwischen 3 und 5 Zeichen a in Folge enthalten. Das Muster (bla){3} paßt auf alle Strings, die genau die Zeichenfolge blablabla enthalten und das Muster ^a(.{3,})b$ paßt auf alle Strings, die mit einem a anfangen, mit einem b enden und die dazwischen mindestens 3 Zeichen enthalten. Welche (und wieviele) Zeichen dies waren, kann man nach Anwendung des Musters abfragen, da dieser Bereich im Muster mit runden Klammern markiert war.

  • Das Fragezeichen ? kann als Kürzel für {0,1} verwendet werden: Es steht für null oder eine Wiederholung des vorhergehenden Ausdrucks.

  • Der Stern * steht für {0,}, also für Null oder mehr Wiederholungen des vorhergehenden Ausdrucks.

  • Das Pluszeichen steht für {1,}, also für mindestens eine Wiederholung des vorhergehenden Ausdrucks.

Eckige Klammern leiten eine Zeichenmenge ein. Eine Zeichenmenge steht immer für genau ein Zeichen und zwar für ein Zeichen, das in der Menge enthalten ist. Also paßt der reguläre Ausdruck [0123456789] auf genau eine beliebige Ziffer. Innerhalb der eckigen Klammern einer Zeichenmenge haben die folgenden Zeichen eine besondere Bedeutung:

  • Der Backslash \ gilt als Escapezeichen: Das ihm folgende Zeichen hat keine besondere Bedeutung, sondern steht nur für sich selbst.

  • Das Dach ^ als erstes Zeichen einer Zeichenmenge (aber nur als erstes Zeichen!) negiert die Zeichenmenge. Die Zeichenmenge [^0123456789] steht also für genau ein Zeichen, das keine Ziffer ist.

  • Das Minuszeichen - definiert einen Bereich. Die Zeichenmenge [0-9] ist also eine kürzere Schreibweise als [0123456789] für dieselbe Menge.

  • Die besonderen Zeichenmengen \d (eine dezimale Ziffer), \D (die Negation von \d), \w (ein "Wort"-Zeichen), \W (die Negation von \w), \s (ein "Whitespace"-Zeichen) und \S (die Negation von \s) stehen für die jeweils angegebenen Zeichen.

Verwendet man einen Suchausdruck mit den preg*-Funktionen, dann ist der Suchausdruck in sogenannte Begrenzerzeichen (Delimiter) einzuschließen, hinter denen noch Optionen mit angegeben werden können. Meistens verwendet man entweder die Schrägstriche / oder Gleichheitszeichen =.

Aus diesen Komponenten kann man sich mit einiger Übung Suchausdrücke zusammenbauen, die nicht nur weitgehend unlesbar sind, sondern die außerdem schnell und schmerzlos die gewünschten Suchfunktionen oder Such- und Ersetzefunktionen durchführen.

8.6. Wie teste ich auf die Existenz mehrerer Suchworte in einem String?

Keywords: Anwendung | Beispiel

Antwort von Johannes Frömter

Der einfachste Anwendungsfall von preg_match() ist zu testen, ob ein Suchmuster auf einen gegebenen String paßt. Beispiele:

# Kommt "Wort" in $eingabe vor?
preg_match("/Wort/", $eingabe);

# Kommt "Wort", "wort" oder "wOrT" etc. in $eingabe vor?
preg_match("/wort/i", $eingabe);

# Kommt "Wort" am Anfang oder am Ende von $eingabe vor?
preg_match("/^Wort|Wort$/", $eingabe);

# Kommt "Wort", "Wart", "Wirt" oder "Wert" in $eingabe vor?
preg_match("/W[aeio]rt/", $eingabe);

# Kommt "Wort" oder "Word" in $eingabe vor?
preg_match("/Wor(t|d)/", $eingabe);

# Kommt "DM " oder "TDM " mit einer zwei- bis
# dreistelligen Zahl in $eingabe vor?
preg_match("/T?DM \d{2,3}/", $eingabe);

# Kommt "Word " mit einer Versionsnummer (z.B. 7.0 oder 97)
# in $eingabe vor? (\d+ paßt auf "eine oder mehr" Ziffern,
# \.? KANN ein Punkt sein, \d* sind "Null oder mehr" Ziffern)
preg_match("/Word \d+\.?\d*/", $eingabe);

8.7. Wie isoliere ich Suchstrings aus einem größeren Text?

Keywords: Tag | Grabbing | mehrzeilig | Modifier

Antwort von Kristian Köhntopp

Das folgende vollständige Beispiel zeigt, wie man den Inhalt des Body-Tags aus einer HTML-Datei isolieren kann.

  $str = "lalalala
<body bgcolor=#cccccc>lang und weilig
noch eine zeile
<h1>Bla</h1>
</body>
tralalal";
  preg_match_all("=<body[^>]*>(.*)</body>=siU", $str, $a);
  print $a[1][0];

Das Beispiel macht von den Optionen i, s und U der Perl Regular Expressions Gebrauch: Die Option i sorgt dafür, dass Groß- und Kleinschreibung keine Rolle spielen, die Option U sorgt dafür, dass Ungreedy gematched wird, d.h. der kürzest mögliche Match verwendet wird. Die Option s bewirkt, dass der Punktoperator auch Newlines mit matched. Dadurch ist es möglich, den regulären Ausdruck auf auf einen mehrzeiligen String anzuwenden.

8.8. Wie finde ich alle Links in einer HTML-Datei?

Keywords: Hyperlink

Antwort von Björn Schotte

$zeile sei der Inhalt einer zuvor eingelesenen HTML-Datei. Diese Variable muss innerhalb der While-Schleife neu zusammengebaut werden, sonst läuft man hier in eine Endlosschleife.

$pattern = '=^(.*)<a(.*)href\="?(\S+)"([^>]*)>(.*)</a>(.*)$=msi';
while (preg_match($pattern, $zeile, $txt))
{
  /* $txt[3] enthält die gewünschte URL. */
  echo $txt[3]."\n";

  /* $zeile neu bauen */
  $zeile = $txt[1]." hier war mal ein Link ".$txt[6];
}

/* $zeile zur Kontrolle ausgeben */
print "<br>".nl2br($zeile);

$txt enthält als Array alle Tokens, die in der Regexp in Klammern angegeben sind. $txt[0]als Sonderstellung enthält den ganzen Text.

8.9. Wie ersetze ich alle relativen Links in einer HTML-Datei?

Keywords: Hyperlink | relativ | absolut

Antwort von Kerry W. Lothrop

$zeile sei der Inhalt einer zuvor eingelesenen HTML-Datei. Im folgenden Beispiel werden alle relativen Links durch das Konstrukt <?php echo $sess->purl("relativerlink"); ?> ersetzt. relativerlink sei hierbei der relative Link, der gefunden wurde.

$pattern  = ',<a([^>]+)href="(?!https?://|ftp://|mailto:|news:)([^>"\s]+)",i';
$replacement = '<a\1href="<?php echo $sess->purl("\2"); ?>"';

$newtext = preg_replace($pattern, $replacement, $text);

header('Content-type: text/plain');
echo $newtext;

8.10. Wie überprüfe ich einen String auf seinen Inhalt?

Keywords: pruefen | testen

Antwort von Martin Jansen

Häufig ist es nötig, festzustellen, ob ein String nur Ziffern bzw. nur Buchstaben enthält.

$string sei die Zeichenkette, die überprüft werden soll. Die Regular Expression im ersten Beispiel überprüft, ob nur Ziffern in $string enthalten sind. Ist dies der Fall, gibt sie "Zeichenkette OK" aus, ansonsten lautet die Ausgabe "Ungültiges Zeichen in der Zeichenkette".

/* Regex zur Ueberpruefung des Strings */
if (!preg_match("/^\d+$/",$string)) {
  echo "Ungültiges Zeichen in der Zeichenkette";
} else {
  echo "Zeichenkette OK";
}

Um zu überprüfen, ob in der Zeichenkette nur Buchstaben stehen, kann man folgende Regex verwenden, die auf dem gleichen Prinzip beruht:

if (!preg_match("=^[a-zäöüß]+$=i",$string)) {
  echo "Ungültiges Zeichen in der Zeichenkette";
} else {
  echo "Zeichenkette OK";
}

8.11. Wie ersetze ich in einem Text, jedoch nicht innerhalb von HTML-Tags?

Keywords: ersetzen | begrenzen

Antwort von Johannes Frömter

Mit Regulären Ausdrücken kann man zwar wunderbar "positive Treffer" formulieren, aber das Gegenteil davon geht nur sehr schwer (abgesehen von negierten Zeichenklassen und Lookaheads/-behinds geht es nicht). Die Entscheidung, ob ein Treffer innerhalb eines HTML-Tags (also zwischen < und >) liegt oder nicht, muss man von PHP treffen lassen. Hierzu gibt es den Modifier e, der PHP das zweite Argument von preg_replace() als PHP-Code auswerten läßt.

Mit folgender Konstruktion kann man in $t alle Vorkommen von $s außerhalb von < und > durch $r ersetzen; die zweite Version ist besonders mit dem Modifier i interessant, um Wörter unabhängig von ihrer Groß-/Kleinschreibung unter Beibehaltung der Schreibweise hervorzuheben:

// $s in $t durch $r ersetzen:
preg_replace("/((<[^>]*)|$s)/e", '"\2"=="\1"? "\1":"$r"', $t);

// $s case-insensitive in $t hervorheben:
preg_replace("/((<[^>]*)|$s)/ie", '"\2"=="\1"? "\1":"<b>\1</b>"', $t);

8.12. Wie mache ich aus URIs im Text anklickbare Links?

Keywords: Hyperlink | HTML

Antwort von Björn Schotte

Besten Dank an Thomas Weinert, von dem die ursprüngliche RegExp stammt.

Folgender regulärer Ausdruck ersetzt alle normalen URIs, das heißt zum Beispiel http://www.phpcenter.de/, news:de.comp.lang.php, mailto:bjoern@thinkphp.de oder ftp://ftp.suse.com/ durch HTML-Code, damit diese URIs für den Benutzer klickbar werden.

/**
* replace URIs with appropriate HTML code to be clickable.
*/
function replace_uri($str) {
  $pattern = '#(^|[^\"=]{1})(http://|ftp://|mailto:|news:)([^\s<>]+)([\s\n<>]|$)#sm';
  return preg_replace($pattern,"\\1<a href=\"\\2\\3\"><u>\\2\\3</u></a>\\4",$str);
}

8.13. Hilfe, mein Regulärer Ausdruck frißt zuviel!

Keywords: ungreedy

Antwort von Johannes Frömter

Setzt man in einem Regulären Ausdruck sogenannte Quantifier (?, *, + und {n,m}) ein, weil ein Teil des Ausdrucks in variabler Anzahl vorkommen darf, so versuchen diese grundsätzlich, so viele Zeichen wie nur möglich zu "fressen" (ohne dabei den Ausdruck scheitern zu lassen). Diese Eigenschaft wird "gierig" (engl. "greedy") genannt. Da dieses Verhalten nicht immer gewünscht ist, läßt sich die Gierigkeit (Greediness) umschalten - ein Quantifier frißt dann nur so viele Zeichen wie nötig, jedoch so wenig wie möglich.

Diese Umschaltung läßt sich entweder durch den Modifier U (wie ungreedy) für alle Quantifier im gesamten Ausdruck, oder durch ein nachgestelltes ? (Fragezeichen) für einen einzelnen Quantifier realisieren. Auch eine Kombination ist möglich - d.h. erst mittels U die Quantifier normalerweise ungreedy zu machen, einzelne davon durch ein ? jedoch wieder greedy. Anhand folgender Beispiele ist zu sehen, wieviele Zeichen der Ausdruck .* je nach Greediness verschlingt - mal ist er mit dem erstbesten r zufrieden, mal nimmt er alles bis zum letztmöglichen r mit:

$string  = 'Dieser Satz wird fast gefressen';

// Normalzustand:
preg_match('/D.*r/', $string, $matches);
-> Dieser Satz wird fast gefr

// Einen Quantifier ungreedy gemacht:
preg_match('/D.*?r/', $string, $matches);
-> Dieser

// Greediness aller Quantifier umgeschaltet:
preg_match('/D.*r/U', $string, $matches);
-> Dieser

// Doppelt gemoppelt: Greediness umgeschaltet,
// einen Quantifier wieder zurückgeschaltet:
preg_match('/D.*?r/U', $string, $matches);
-> Dieser Satz wird fast gefr

// Bei dieser Schreibweise spielt die Greediness keine Rolle:
preg_match('/D[^r]*r/', $string, $matches);
-> Dieser

Die ereg()-Funktionen kennen übrigens keine umschaltbare Gierigkeit (d.h. Quantifier sind immer gierig).

8.14. Wie ersetze ich alle relativen Links in einer HTML-Datei durch absolute?

Keywords: Hyperlink | relativ | absolut

Antwort von Kerry W. Lothrop

In diesem Beispiel wird die HTML-Datei über den file() -Befehl vom entfernten Server gezogen. Die Funktion absolute() erwartet als ersten Parameter einen Link und als zweiten Parameter die volle URL des Dokuments, in dem sich der Link befindet. Sie kann auch in einem anderen Kontext verwendet werden. Obwohl viele der Anweisungen (z.B. substr() oder strrpos() ) auch über reguläre Ausdrücke gelöst werden können, wurde aus Performance-Gründen bewusst darauf verzichtet.

<?php
// Datei über HTTP aufrufen
$url = 'http://www.server.de/test.html';
$old = implode('', file($url));

// Links suchen und an absolute() weiterleiten
$new = preg_replace(',<a([^>]+)href="([^>"\s]+)",ie',
    '"<a\1href=\"" . absolute("\2", $url) . "\""',
    $old);

// HTML-Code ausgeben
header('Content-type: text/plain');
echo $new;

// Funktion, die relative in absolute Links umschreibt
function absolute ($relative, $absolute) {

    // Link ist schon absolut
    if (preg_match(',^(https?://|ftp://|mailto:|news:),i', $relative))
        return $relative;

    // parse_url() nimmt die URL auseinander
    $url = parse_url($absolute);
    
    // dirname() erkennt auf / endende URLs nicht
    if ($url['path']{strlen($url['path']) - 1} == '/')
        $dir = substr($url['path'], 0, strlen($url['path']) - 1);
    else
        $dir = dirname($url['path']);

    // absoluter Link auf dem gleichen Server
    if ($relative{0} == '/') {
        $relative = substr($relative, 1);
        $dir = '';
    }

    // Link fängt mit ./ an
    elseif (substr($relative, 0, 2) == './')
        $relative = substr($relative, 2);

    // Referenzen auf höher liegende Verzeichnisse auflösen
    else while (substr($relative, 0, 3) == '../') {
        $relative = substr($relative, 3);
        $dir = substr($dir, 0, strrpos($dir, '/'));
    }

    // volle URL zurückgeben
    return sprintf('%s://%s%s/%s', $url['scheme'], $url['host'], $dir, $relative);
}
?>

9. Arrays und Arrayvariablen

9.1. Wie kann ich ein Element an ein Array anfügen?

Keywords: erzeugen | einfuegen | Element

Antwort von Kristian Köhntopp

Durch Verwendung des leeren Arrayoperators wird an ein Array ein Element angehängt. In Code:

$avar[] = "neues element";

In PHP 4 kann man mit der Funktion array_push() auch mehrere Elemente gleichzeitig an ein Array anfügen.

Durch Verwendung eines Index kann man ein Element an einer bestimmten Stelle in einem Array ansprechen. Der Index kann numerisch oder ein String sein:

$avar[1]     = "Element mit dem Index 1";
$avar["bla"] = "Element mit dem Index 'bla'";

9.2. Wie kann ich ein Array aufzählen?

Antwort von Kristian Köhntopp

Ein Array enthält $anz = count($avar) viele Elemente. Man kann diese Elemente mit einer for-Schleife aufzählen, falls die Indizes numerisch-zusammenhängend sind:

$avar = array('rot', 'gelb', 'blau', 'schwarz');
$anz = count($avar);
for ($i=0; $i<$anz; $i++) {
    printf("i: %d avar[%d] = %s<br>\n", $i, $i, $avar[$i]);
}

Für assoziative Arrays ist dieses Konstrukt besser geeignet:

if (isset($avar) && is_array($avar)) {
    foreach ($avar as $k => $v) {
        printf("k=%s v=%s<br>\n", $k, $v);
    }
}

In PHP3 hat man oft die folgende Syntax verwendet, die inzwischen veraltet (aber noch funktionsfähig) ist:

if (isset($avar) && is_array($avar)) {
    reset($avar);
    while (list($k, $v) = each($avar)) {
        printf("k=%s v=%s<br>\n", $k, $v);
    }
}

Es macht Gebrauch von den Funktionen reset() um den internen Positionszeiger eines Arrays zurückzusetzen, list() um einen Zuweisungskontext für ein Wertepaar $k und $v zu erzeugen und each() um den Schlüssel (key, k) und den Wert (value, v) an der aktuellen Position des Arrays auszulesen.

Von der Anwendung der veralteten und defekten Funktionen next() , prev() und current() ist in diesem Zusammenhang abzuraten, da sie bei Arrays mit Nullwerten falsche Ergebnisse liefern. Diese Schleife wird nur die Werte -2 und -1 ausgeben, da hier der Wert 0 nicht vom Feldende unterschieden werden kann:

$avar = array(-2, -1, 0, 1, 2);
for (reset($avar); $v = current($avar); next($avar)) {
    printf("v = %d<br>\n", $v);
}

9.3. Wie kann ich ein Element aus einem Array löschen?

Keywords: Array | loeschen | Element | entfernen

Antwort von Johannes Frömter

Mit unset() kann man sowohl Variablen (Strings, ganze Arrays etc.) als auch einzelne Elemente eines Arrays löschen:

$array = array('P', 'H', 'P');

// Löscht das 'H'
unset($array[1]);

// Löscht das ganze Array
unset($array);

Durch das Löschen einzelner Einträge entstehen Lücken im Index des Arrays (d.h. $array[1] greift ins Leere); die Array-Funktionen selbst (wie z.B. foreach() oder each() ) stört dies jedoch nicht. Benötigt man dennoch ein Array mit fortlaufendem Index für direkten Zugriff, muss man es aus den verbleibenden Elementen neu erstellen:

// Array mit fortlaufendem Index erzeugen
$array = array_values($array);

Weiterhin kann man auch mit array_splice() Teile eines Arrays entfernen, und dabei automatisch einen zusammenhängenden numerischen Index erzeugen lassen (falls das benötigt wird):

// In $array ab Index 1 ein Element durch nichts ersetzen
array_splice($array, 1, 1);

9.4. Wie greife ich auf ein mehrdimensionales Array zu?

Keywords: Dimension

Antwort von Kristian Köhntopp

Das lässt sich am einfachsten an einem kleinen Beispiel zeigen:

$array = array(
   "foo" => array ("a", "b"),
   "bar" => array ("c", "d")
   );

print($array["foo"][0]); // gibt "a" aus.
print($array["bar"][1]); // gibt "d" aus.

9.5. Wie kann ich ein Array nach einem beliebigen Kriterium sortieren lassen?

Antwort von Kristian Köhntopp

PHP stellt eine Reihe von vordefinierten Sortierfunktionen zur Verfügung. Wenn diese nicht ausreichen, kann man mit Hilfe der Funktion usort() nach beliebigen Kriterien sortieren lassen. Der Funktion muss eine Vergleichsfunktion und das zu sortierende Array als Parameter mit übergeben werden.

Das nachfolgende Beispiel sortiert ein Array von Paaren alphabetisch nach dem 2. Element.

kris@valiant:~ > ./php
<?php
  $a = array(
         array(0, "Schmidt"),
         array(2, "Albert")
       );

  function cmp($a, $b) {
    printf("type a = %s type b = %s\n", gettype($a), gettype($b));
    if ($a[1] == $b[1]) return 0;
    return ($a[1] > $b[1])?1:-1;
  }

  usort ($a, "cmp");

  foreach($a as $k => $v)
      printf("k = %s  v[0] = %s  v[1] = %s\n", $k, $v[0], $v[1]);
?>
X-Powered-By: PHP/4.0b5-dev
Content-type: text/html; charset=iso-8859-1

type a = array type b = array
k = 0  v[0] = 2  v[1] = Albert
k = 1  v[0] = 0  v[1] = Schmidt
kris@valiant:~ >

Antwort von Johannes Frömter

Weiterhin gibt es die Funktion array_multisort() , die - gefüttert mit einem passenden (eindimensionalen) "Sortierarray" - mehrdimensionale Arrays nach beliebigen Dimensionen ordnen kann.

foreach($a as $v) $s[] = $v[1];    // [1] = Dimension zur Sortierung
array_multisort($s, SORT_ASC, $a); // ASC = auf-, DESC = absteigend

Durch Einfügen weiterer "Sortierarrays" ($s) vor dem "Nutzarray" $a kann man auch nach mehreren Kriterien gleichzeitig sortieren lassen. Dabei kann nach jedem "Sortierarray" die Sortierrichtung und -art neu bestimmt werden.

9.6. Wie kann ich Duplikate aus einem Array entfernen?

Keywords: Mengen

Antwort von Johannes Frömter

Seit PHP Version 4.0.1 gibt es die Funktion array_unique() , um doppelte Einträge in Arrays zu eliminieren.

Anmerkung: array_unique() behält die Indizes des Original- Arrays bei. Um auf die Elemente des Arrays mit fortlaufenden numerischen Indizes von 0 bis count()-1 zugreifen zu können, verwende man

$a = array_values(array_unique($a));

In PHP3 kann man sich z.B. mit folgender Konstruktion behelfen: Wie kann ich Duplikate aus einem Array entfernen?

9.7. Wie kann ich ein Array von einer Seite auf eine andere transportieren?

Keywords: Session

Antwort von Martin Jansen

Die einzig vernünftige und sichere Methode, um ein Array von einer Seite auf eine andere Seite zu transportieren, ist das Ablegen des Arrays in einer Session auf dem Server. Alle anderen Methoden stellen ein Sicherheitsrisiko dar und sollten nicht angewendet werden.

Weitere Informationen zu Sessions finden sich in Sessions und in Was sind Sessions und warum sind sie nützlich?

Im Prinzip kann man ein Array mit serialize() in einen String verwandeln und per URL weiterreichen (URL-Codierung etc. nicht vergessen), aber das ist wegen der begrenzten Länge von URLs und der Manipulationsmöglichkeit durch den Benutzer zu vermeiden.

10. Klassen und Objekte

10.1. Warum Klassen und Objekte benutzen?

Antwort von Kristian Köhntopp

Der Artikel Data Driven Websites mit PHP, Teil 2 zeigt, dass es recht schwierig ist, Code zu bauen, der sowohl komfortabel als auch wiederverwendbar ist.

Mit Hilfe von Klassen läßt sich solcher Code so kapseln, dass er vergleichsweise störungsfrei in existierende Projekte eingesetzt werden kann, ohne Gefahr zu laufen, mit bereits benutzten Funktions- und Variablennamen zu kollidieren.

10.2. Wie definiere ich eine Klasse? Wie erzeuge ich ein Objekt?

Antwort von Kristian Köhntopp

Angenommen, es ist eine Reihe von Funktionen vorhanden, die mit einer Datenbank kommunizieren und diese Funktionen sollen in eine Klasse umgewandelt werden:

 $Link_ID  = 0;  // ID der aktuellen DB-Verbindung
 $Query_ID = 0; // ID des aktuellen Abfrageresultates
 $Error    = 0;    // Letzte Datenbank-Fehlermeldung

 function connect() { ... }

 function query()   { ... }

 function next_record() { ... }

 function num_rows() { ... }

Aus diesen Variablen und Funktionen wird eine Klasse, indem man vor alle verwendeten Variablen das Schlüsselwort var schreibt und indem man alle Variablen und Funktionen mit einem class-Konstrukt umschließt.

class DB_MiniSQL {
 var $Link_ID  = 0; // ID der aktuellen DB-Verbindung
 var $Query_ID = 0; // ID des aktuellen Abfrageresultates
 var $Error    = 0; // Letzte Datenbank-Fehlermeldung

 function connect() { ... }

 function query()   { ... }

 function next_record() { ... }

 function num_rows() { ... }
}

Klassen selbst sind nur Baupläne, sie erzeugen keine Variablen und die Funktionen, die in ihnen enthalten sind, lassen sich so nicht verwenden. Mit Hilfe der Anweisung new läßt man den PHP-Interpreter eine Variable, ein Objekt, nach diesem Bauplan bauen.

 $db1 = new DB_MiniSQL; // $db1 ist ein Objekt der Klasse DB_MiniSQL
 $db2 = new DB_MiniSQL; // $db2 ist noch ein Objekt derselben Klasse

Das Objekt $db1 kann man sich wie ein Array mit einer besonderen Syntax vorstellen. Anstatt auf $db1["Link_ID"] und $db1["Error"] zuzugreifen, muss man $db1->Link_ID und $db1->Error verwenden. Auch die Funktionen in einem Objekt lassen sich so aufrufen: $db1->connect(), $db1->query() und so weiter.

Ein beliebter Fehler ist, $db1->Error zu meinen, aber $db1->$Error zu schreiben. Das ist falsch: Der vollständige Name der Variablen ist db1->Error, mit einem $ davor, um ihn als Variablennamen zu kennzeichnen.

10.3. Was ist $this?

Keywords: Klasse | Objekt | this | Name | Funktion

Antwort von Kristian Köhntopp

Innerhalb einer Funktion wie connect() muss auf die Variable Link_ID zugegriffen werden, um das Resultat eines Connect abzuspeichern. In connect() können wir nicht wissen, wie die Funktion nun gerade heißt, also ob ihr Name nun gerade $db1->connect() oder $db2->connect() ist und ob die Link-ID nun in $db1->Link_ID oder in $db2->Link_ID abgespeichert werden muss.

Eigentlich ist das auch egal: Wir wollen ja nur auf unsere eigene Link-ID zugreifen. $this bezeichnet nun genau unser eigenes Objekt, also $db1 innerhalb von $db1 und $db2 innerhalb von $db2. Man schreibt daher code wie

class DB_MiniSQL {
  var $Link_ID = 0;

  function connect() {
    $this->Link_ID = mysql_connect(...);
    ...
  }

  ...
}

oder

class DB_MiniSQL {
  var $Link_ID = 0;

  function query($query) {
    // Wenn kein Datenbank-Link vorhanden ist, eines herstellen.
    if (!$this->Link_ID)
      $this->connect();

    ...
   }
  ...
}

10.4. Was ist extends? Was ist Vererbung?

Antwort von Kristian Köhntopp

Häufig braucht man eine Klasse, die sich genauso verhält wie eine Klasse, die man schon hat, aber mit ganz kleinen Änderungen. Mit Hilfe des Schlüsselworts extends kann man sich eine Klasse definieren, die genauso ist wie eine bereits existierende Klasse und braucht dann nur noch das zu notieren, was anders ist.

Die Änderungen können dabei eine bestehende Klasse erweitern, also neue Variablen und Funktionen zu einer Klasse hinzufügen oder bestehende Variablen und Funktionen einer Klasse ersetzen.

Der folgende Beispiel-Code definiert eine Klasse Example_SQL, die sich ganz genauso verhält wie die Klasse DB_Sql in PHPLIB. Die Variablen $Host, $User, $Password und $Database sind jedoch anders belegt als in der originalen Klasse: Wir setzen dort einfach die Informationen ein, die notwendig sind, um unsere Datenbank zu kontaktieren. Außerdem ist die Funktion haltmsg() ersetzt. Die Klasse ruft diese Funktion auf, wenn ein Fehler aufgetreten ist. Wir ersetzen diese Funktion durch eine eigene Version, sodass wir Fehlermeldungen mit den Informationen drucken können, die der Anwender benötigt.

class Example_Sql extends DB_Sql {
  var $Host     = "database.netuse.de";
  var $User     = "kris";
  var $Password = "xyzzy";
  var $Database = "example_database";

  function haltmsg($msg) {
?>
    Es ist ein Datenbankfehler aufgetreten. Die Bearbeitung
    Ihrer Eingaben wurde abgebrochen. Bitte informieren Sie
    <a href="mailto:webmaster@example.kunde.de">den Webmaster</a>
    von diesem Problem.<p>

<?php
    printf("Die Fehlermeldung der Datenbank war: %s\n", $msg);
  }
}

Man kann die Klasse Example_SQL nicht verstehen, ohne die originale Klasse DB_SQL zu kennen. Die Klasse Example_SQL hat alle Eigenschaften und Funktionen, die DB_SQL auch hat. Erzeugt man also ein $db = new Example_SQL, dann kann man $db->query(...), $db->next_record() und so weiter aufrufen, als ob man es mit einer Klasse DB_SQL zu tun hätte.

Die Klasse zeigt nur in folgenden Punkten abweichendes Verhalten: Sie druckt ihre Fehlermeldungen in deutsch und enthält anwendungsspezifische Kontaktinformationen und sie kontaktiert anders als die Originalklasse defaultmäßig die Datenbank example_database auf dem Host database.netuse.de mit dem angegebenen Usernamen und Passwort.

10.5. Was ist ein Konstruktor?

Antwort von Kristian Köhntopp

Ein Konstruktur ist eine gewöhnliche Funktion einer Klasse. Sie unterscheidet sich von anderen Funktionen derselben Klasse dadurch, dass sie beim Erzeugen der Klasse mit new automatisch aufgerufen wird. In PHP muss ein Konstruktor unglücklicherweise genauso heißen wie die Klasse selbst.

Ein Konstruktor kann optionale Parameter mitgegeben bekommen. Er kann niemals ein Funktionsergebnis liefern.

Man verwendet Konstruktoren oft, um die Variablen eines Objektes zu initialisieren. Die Klasse Menu in PHPLIB verwendet beispielweise einen Konstruktor, um eine Menüstruktur zu initialisieren.

class Menu {
  function Menu() {   // wird automatisch aufgerufen
    $this->setup();
  }

  function setup() {  // Initialisierung...
    reset($this->urlmap);
    while(...) {
      ...;
    }
  }
}

In PHP3 wurde bei abgeleiteten Klassen der Konstruktor der Basisklasse nicht automatisch aufgerufen. Man musste stattdessen Code wie diesen schreiben:

class My_Menu extends Menu {
  var $urlmap = array(
    // meine eigenen Menüpunkte hier
  );

  function My_Menu() {   // wird automatisch aufgerufen
    $this->setup();      // Aufruf der Initialisierung
  }
}

10.6. Was sind polymorphe Funktionen? Kann ich sie simulieren?

Antwort von Kristian Köhntopp

Unter Polymorphie versteht man das Verhalten von objektorientierten Sprachen, die Signatur einer Funktion als Bestandteil des Funktionsnamens bei einem Aufruf zu betrachten. Die Signatur einer Funktion sind der Returntyp und die Parametertypen einer Funktion. In einer Sprache mit Polymorphie würde Code wie der folgende funktionieren:

# Funktion f mit Integer-Resultat und Integer-Parametern
function int f(int $a, int $b) {
  return $a*$b;
}

# Funktion f mit Array-Resultat und Array-Parametern
function array f(array $a, array $b) {
  $r = array();
  $l = count($a);
  for ($i=0; $i<$l; $i++)
    $r[] = $a[$i] * $b[$i];
  return $r;
}

# Definition von Integer-Parametern
$xi = 3;
$yi = 4;

# Aufruf der Integer-Funktion f.
$zi = f($xi, $yi);

# Definition von Array-Parametern
$xa = array(2, 3, 4);
$ya = array(4, 3, 2);

# Aufruf der Array-Funktion f.
$za = f($xa, $ya);

Dieser Code definiert zwei verschiedene Funktionen, die intern als int_f_int_int() und array_f_array_array() bezeichnet werden können, die im Code aber beide ununterscheidbar f() heißen. Er ruft dann die Funktion f() einmal mit Integer-Parametern und Array-Paramerern auf. Die Sprache ist aufgrund der Polymorphie in der Lage, diese beiden f() zu unterscheiden und korrekt die Funktion int_f_int_int() oder array_f_array_array() aufzurufen.

PHP unterstützt keine Polymorphie und kann dies schon deswegen nicht tun, weil die Return- und Parametertypen einer Funktion nicht deklariert werden müssen. Stattdessen muss man manuell mit den Typfunktionen wie folgt codieren:

function f($a, $b) {
  # Wandle $a in ein Array um, wenn es das nicht ist.
  if (!is_array($a))
    $a = array($a);

  # Ebenso $b.
  if (!is_array($b))
    $b = array($b);

  # Normaler Code.
  $r = array();
  $l = count($a);
  for ($i=0; $i<$l; $i++)
    $r[] = $a[$i] * $b[$i];

  if (count($r) == 1)
    # Skalar zurückgeben
    return $r[0];
  else
    # Array zurückgeben
    return $r;
}

10.7. Wie kann ich Metainformationen über eine Klasse bekommen?

Antwort von Kristian Köhntopp

Metainformationen über eine Klasse oder ein Objekt sind alle Informationen, die man über diese Klasse oder eine Instanz dieser Klasse (ein Objekt) bekommen kann. Sie umfassen den Namen der Klasse eines Objektes und die Namen aller Oberklassen dieser Klasse, die Namen und Typen aller Instanzvariablen des Objektes und die Namen, Returntypen sowie Parametertypen aller Funktionen eines Objektes.

Man kann folgende Metadaten über Klassen bzw. Objekte bestimmen:

10.8. Wie speichere ich ein Objekt in einer Session?

Antwort von Clemens Koppensteiner

Die Antwort findest Du jetzt unter "Wie speichere ich Objekte in Sessions?" im Kapitel "Sessions".

10.9. Wie komme ich an den Namen eines Objekts?

Keywords: Objekt | Metainformationen | Name

Antwort von Richard Körber

Der Programmierer hat mit new MyClass() ein neues Objekt erzeugt, und möchte nun innerhalb der Klasse MyClass wissen, in welcher Variablen das Objekt abgelegt wurde.

Innerhalb des Objekts kann man hier stets über die Pseudovariable $this auf sich selbst zugreifen (siehe "Was ist $this?").

Einen konkreten Variablennamen zu nennen, in der das Objekt abgelegt wurde, ist jedoch nicht möglich, und aus OOP-Sicht auch überhaupt nicht notwendig.

PHP kann beliebig viele Referenzen auf dasselbe Objekt verwalten. Ein Objekt kann damit über mehrere Variablen erreichbar sein oder sogar anonym sein. Beispiel:

$a =& new MyClass();
$b =& $a;

Wäre der Name des Objekts jetzt $a oder $b?

11. Variablen und Formulare

11.1. Wie übergebe ich Variablen aus einem Formular an ein PHP-Script?

Keywords: Formular | Uebergabe | Variable

Antwort von Kerry W. Lothrop

Gar nicht. Wenn das action=-Attribut eines Formulares ein PHP-Script ist, dann stehen die Variablen aus dem Formular und aus den Cookies automatisch als Elemente in einem von drei Arrays in PHP zur Verfügung: Je nach der Art der Übergabe stehen sie in $_GET, $_POST oder $_COOKIE bereit. Früher standen sie außerdem automatisch als globale Variablen bereit, aber dies ist ein Sicherheitsrisiko und seit PHP 4.2.x nicht mehr der Fall. Der Schalter register_globals kontrolliert dieses Verhalten und er steht seit PHP 4.2.x per Default auf off (siehe Warum funktionieren meine Formulare nicht?).

Wenn die Quelle der Daten nicht bekannt ist (z.B. wenn das Formular über GET oder POST aufgerufen werden darf), kann auch das Array $_REQUEST verwendet werden, das die Daten der drei Arrays, die externe Daten enthalten, vereint. Wenn gleichzeitig gleichnamige Variablen über unterschiedliche Methoden übergeben werden (z.B. GET-Variable und Cookie), werden sie in der unter variables_order angegebenen Reihenfolge berücksichtigt bzw. überschrieben.

Weil es sich bei diesen Arrays um superglobale Variablen handelt, sind sie in Funktionen automatisch sichtbar. Sie müssen nicht mit global() importiert werden.

11.2. Wie kann ich ohne Formular Variablen an ein Script übergeben?

Antwort von Kristian Köhntopp

Wenn GET-Variablen Zeichen enthalten bzw. zur Laufzeit enthalten können, die nicht im Klartext in URLs auftauchen dürfen (Umlaute, Leerzeichen, Prozentzeichen etc.), muss man die Variablen mit urlencode() codieren, bevor man sie an die URL anhängt. Um die Decodierung muss man sich im Normalfall nicht kümmern, das geschieht automatisch. Mit folgendem Script lassen sich mehrere Werte - übergeben als array(Variable => Wert) - bequem codieren:

<?php
function req_url($url, $para) {
  $sep = "?";

  if (! is_array($para))
    return $url;

  foreach ($para as $k => $v) {
    $url .= sprintf("%s%s=%s",
      $sep,
      $k,
      urlencode($v)
    );
    $sep = "&amp;";
  }

  return $url;
}

$p = array(
  "a" => "b",
  "c" => "d"
);

$url = req_url("beispiel.php", $p);
?>
Klicke auf das <a href="<?php print $url ?>">Beispiel</a>.

11.3. Wie viele Formularelemente kann ich auf einer Seite haben?

Keywords: Anzahl | Formular | Elemente | Limit | URL | GET | POST

Antwort von Kristian Köhntopp

Wird das Formular mit POST übergeben, ist die Anzahl und Größe der Elemente möglicherweise begrenzt durch serverseitige Einstellungen (Apache: siehe LimitRequestBody und verwandte Direktiven).

Wird das Formular mit GET übergeben, ist die Anzahl der Variablen begrenzt durch die maximale Länge der URL, die der Browser und der Webserver verarbeiten können. Beim Browser ist dies vom Browser und der Browserversion abhängig. Beim Webserver ist das Limit unter Umständen konfigurierbar (Apache: siehe LimitRequestLine (8190) und verwandte Direktiven).

11.4. Sollte ich besser GET oder POST verwenden?

Keywords: GET | POST | Formular | Methode | URL

Antwort von Kristian Köhntopp

Im allgemeinen ist es besser, die Methode GET zu verwenden: Formulare sind leichter zu debuggen und der Anwender kann sich ein fertig ausgefülltes Formular mit Parametern in die Bookmarks oder einen Link legen - das ist bequem und ergonomisch.

Enthält das Formular Werte, die nicht in der URL angezeigt werden sollen und die ggf. nicht Bestandteil des Referer sein sollen und nicht in Proxy-Logs auftauchen sollen, dann ist die Verwendung von POST angezeigt. Dies ist zum Beispiel immer der Fall, wenn ein Eingabeelement Password verwendet wird.

Ebenfalls soll POST verwendet werden, wenn die Länge von Eingabeelementen nicht nach oben begrenzt ist, also immer dann, wenn ein TEXTAREA verwendet wird.

Schließlich ist die Verwendung von POST zwingend notwendig, wenn ein File-Upload durchgeführt werden soll, einmal wegen der prinzipiell unbegrenzten Länge, aber auch weil der notwendige ENCTYPE="multipart/form-data" nur mit POST zusammen funktioniert.

11.5. Wie verarbeite ich ein <input type="text">-Feld?

Keywords: Formular | Text | Input | HTML

Antwort von Johannes Frömter

Normale Input-Felder eignen sich für einzeilige Eingaben von 1 bis ca. 100 Zeichen. In HTML werden sie als <input type="text" name="variable"/> definiert, wobei der Inhalt von name in PHP zum Namen der Variable wird, die die Eingabe des Benutzers enthält: $_REQUEST['variable'].

Für eine Vorbelegung des Feldes gibt es das optionale Attribut value:

<input type="text" name="var"
       value="<?php echo htmlspecialchars($_REQUEST['var']); ?>"/>

Da die Variable auch Anführungszeichen enthalten könnte (was das Ende des value-Feldes bedeuten würde), muss man sie durch htmlspecialchars() "entschärfen" lassen.

Formular-Felder werden von PHP immer als Variablen vom Typ string zur Verfügung gestellt - auch wenn das Feld "nichts", oder wenn es nur Zahlen enthält. Siehe hierzu auch: "Wie erkenne ich fehlerhafte/fehlende Eingaben?".

11.6. Wie verarbeite ich eine Textarea?

Keywords: Formular | Text | Input | Textarea | mehrzeilig | HTML

Antwort von Kai Schröder

Dieses Codebeispiel erzeugt für jede Zeile in der Textarea eine Ausgabe, die etwa so aussieht:

Der Inhalt von Zeile 0 ist: "Inhalt der ersten Zeile"

Zu beachten ist, dass die Zählung innerhalb des Arrays $line mit 0 beginnt. Der Code ist für PHP 4.1.0 oder höher geschrieben. Wenn eine älter Version von PHP verwendet wird, so ist $_SERVER["PHP_SELF"] durch $PHP_SELF und $_REQUEST["TA"] durch $TA oder $HTTP_POST_VARS["TA"] zu ersetzen.

<?php
echo '<form action="'.$_SERVER["PHP_SELF"].'" method="post">';
echo '<textarea name="TA" cols="50" rows="10">';
if (isset($_REQUEST["TA"])) {
    echo $_REQUEST["TA"];
}
echo '</textarea>';
echo '<input type="submit" value="Prüfen...">';
echo '</form>';

if (isset($_REQUEST["TA"])) {
    $lines = preg_split("/\r\n/", $_REQUEST["TA"]);
    foreach ($lines as $key => $value){
      echo 'Der Inhalt von Zeile '.$key.' ist: "'.$value.'"<br>';
    }
}
?>

Antwort von Kerry W. Lothrop

Zum Anzeigen des über eine Textarea eingegebenen Textes innerhalb eines HTML-Dokuments eignet sich die Funktion nl2br() (siehe Wie kann ich Zeilenumbrüche in <br> umwandeln?). Innerhalb des HTML-Tags <pre> ist dies jedoch nicht nötig.

11.7. Wie kann ich aus einer Datenbanktabelle einen <select> erzeugen?

Keywords: select | Formular | LOV | option | Auswahlfeld | Liste | Menue | HTML

Antwort von Kristian Köhntopp

Aus einer Datenbanktabelle mit einer List Of Values (LOV) soll eine Selectbox erzeugt werden:

<?php
/*
 * lovselection - wandle eine Liste von Werten aus einer Datenbank
 *                in eine option-Liste in HTML.
 *
 * (benutzt PEAR::DB)
 *
 * von Kristian Köhntopp und Frank Wiegand
 *
 */

function lovselection($dsn, $table, $field, $oldvalue) {

  // Rueckgabestring
  $ret = "";

  // Datenbankverbindung aufbauen, Fehlerbehandlung
  // ist hier nicht implementiert!
  require_once 'DB.php';
  $db = DB::connect($dsn);

  // Daten auslesen
  $query = sprintf("SELECT %s FROM %s",
             $field,
             $table
           );

  $res = $db->query($query);

  while($row = $db->fetchRow()) {
    if ($row[$field] == $oldvalue)
      $selected = " selected='selected'";
    else
      $selected = "";

    $ret .= sprintf("<option%s>%s</option>\n",
                    htmlspecialchars($selected),
                    $row[$field]
                   );
  }

  $db->disconnect();
  return $ret;
}

/*
 * is_validlov - überprüfe, ob ein gegebener Wert zu einer
 *               vorgegebenen Liste von Werten passt.
 *
 * von Kristian Köhntopp und Frank Wiegand
 *
 */
function is_validlov($dsn, $table, $field, $value) {

  require_once 'DB.php';
  $db = DB::connect($dsn);

  $query = sprintf("SELECT %s FROM %s WHERE %s = %s",
             $field,
             $table,
             $field,
             $db->quote($value) // Wert für die Abfrage maskieren
           );

  $res = $db->query($query);
  if ($res->numRows() == 1) {
    $db->disconnect();
    return true;
  } else {
    $db->disconnect();
    return false; // 0 or 2+ answers are a failure!
  }
}

?>

<!-- Formularfragment -->
<select name="f_ortsnetz" size="1">
<?php echo lovselection("mysql://$user:$pass@$host/$db_name", "ortsnetze", "vorwahl", $ortsnetz) ?>
</select>


<!-- Auswertefragment -->
<?php
if (is_validlov("mysql://$user:$pass@$host/$db_name", "ortsnetze", "vorwahl", $_REQUEST['f_ortsnetz']))
  $ortsnetz = $_REQUEST['f_ortsnetz']; // value is valid
else {
  $error["ortsnetz"] = "Das angegebene Ortsnetz ist ungültig.";
  $ortsnetz = ""; // clear session variable
}
?>

Die Funktion lovselection() generiert aus einer Tabelle von Werten in der Datenbank (eine sogenannte List Of Values, LOV) eine Reihe von Option-Tags. Man kann dann leicht den passenden Select-Container drumwickeln. lovselection() verwendet die Datenbank-Klasse DB aus dem PEAR, läuft also mit prinzipiell jeder SQL-Datenbank.

Wenn man einen solchen Select-Tag generiert, dann heißt das natürlich nicht, dass das so erzeugte Formular beim Submit ausschließlich Werte zurückliefert, die man dort zur Auswahl gestellt hat. Stattdessen kann jeder beliebige Wert zurück kommen.

Es ist also notwendig, dass man ein Prädikat erzeugt, das überprüft, ob der eingegangene Wert gültig ist. Dieses Prädikat ist is_validlov(): Die Funktion liefert true, wenn der gegebene Wert genau einmal in der angegebenen Tabelle vorkommt.

Im Beispiel kann mit der Variablen $_REQUEST['f_ortsnetz'], die aus dem Formular kommt, nicht gearbeitet werden - sie kann potentiell ungültige Werte ("003432") enthalten oder sogar potentiell gefährliche Werte ("0431' or sp_clearall() or '0431", wobei sp_clearall() irgendeine Einbaufunktion oder Stored Procedure in einer Datenbank ist, die gefährliche Dinge mit der Datenbank macht).

Der Wert von $_REQUEST['f_ortsnetz'] kann nach $ortsnetz kopiert und gefahrlos verwendet werden genau dann und nur dann, wenn is_validlov() wahr ist, denn dann kommt der Inhalt von $_REQUEST['f_ortsnetz'] genau einmal in der angegebenen Tabelle vor und ist damit eine gültige und garantiert harmlose Auswahl.

Die Funktion is_validlov() selbst kann sich den Luxus nicht erlauben, mit harmlosen Werten zu arbeiten und muss daher so geschrieben sein, dass sie auch mit gefährlichen Inhalten in $value zurecht kommt. Um eine SQL-Injection auszuschließen, wird $value mit der quote-Methode entsprechend für die Abfrage präpariert.

11.8. Wie kann man ein <select multiple> verarbeiten?

Antwort von Kristian Köhntopp

Das Formular muss so aussehen:

<form action="script.php">
<select multiple="multiple" size="3" name="avar[]">
<option value="a">Eins</option>
<option value="b">Zwei</option>
<option value="c">Drei</option>
<option value="d">Vier</option>
<option value="e">Fuenf</option>
<option value="f">Sechs</option>
</select>
<br />
<input type="submit" name="doit" value="Los!" />
</form>

Entscheidend ist, dass der Name der Variablen im <select>-Tag mit eckigen Klammern endet, damit ein Array erzeugt wird. Das Script script.php erhält nun diese Variable $_REQUEST['avar'] (bzw. vor PHP 4.1.0 als $HTTP_GET_VARS['avar']) als Array und kann die Werte dieses Arrays aufzählen.

In PHP3 können auf diese Weise nur eindimensionale Arrays erzeugt werden, in PHP4 sind auch mehrdimensionale Felder möglich. In jedem Fall kann nur die letzte Dimension unbestimmt sein.

11.9. Wie kann man Radio-Buttons verarbeiten?

Antwort von Kerry W. Lothrop

Radio-Buttons verhalten sich analog zu Checkboxen (siehe Wie kann man Checkboxen verarbeiten?), mit der Ausnahme, dass hier eine Mehrfachauswahl nicht möglich ist. Beim Erstellen des HTML-Codes sollte darauf geachtet werden, dass zusammengehörige Buttons den gleichen Namen haben müssen.

Mit dem folgenden Code lassen sich Radio-Buttons analog zu Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform erstellen:

$elements = array(
    array('name' => 'grün', 'value' => 'gr'),
    array('name' => 'blau', 'value' => 'bl'),
    array('name' => 'rot',  'value' => 'ro')
);
foreach ($elements as $element) {
    printf('<input type="radio" name="farbe" value="%s" %s/> %s<br />',
        $element['value'],
        (isset($_REQUEST['farbe']) and $_REQUEST['farbe'] == $element['value']) ? 'checked="checked" ' : '',
        $element['name']);
}

11.10. Wie kann man Checkboxen verarbeiten?

Antwort von Kristian Köhntopp

Wenn die Checkboxen nicht markiert sind, werden sie überhaupt nicht übermittelt. Andernfalls haben sie den im Attribut VALUE= angegebenen Wert. Man kann die Elemente auf die folgenden beiden Arten erzeugen:

# Fall 1: Verschiedene Namen, gleicher Wert
<input type="checkbox" name="cbutton[1]" value="yes" />
<input type="checkbox" name="cbutton[2]" value="yes" />

# Fall 2: "Gleiche" Namen, verschiedene Werte
<input type="checkbox" name="cbutton[]" value="1" />
<input type="checkbox" name="cbutton[]" value="2" />

Die Abfrage erfolgt in beiden Fällen mit

if (isset($_REQUEST['cbutton'])) {
  reset($_REQUEST['cbutton']);
  foreach ($_REQUEST['cbutton'] as $k => $v) {
    print "$k $v\n";
  }
} else {
  print "alle cbutton schlafen schon.\n";
}

Im Fall 1 wertet man die $k aus, im Fall 2 die $v. Entscheidend ist auch hier, dass der Variablennamen bei mehr als einer Checkbox mit [] endet, damit in PHP ein Array zur Verfügung steht.

11.11. Wie funktioniert ein Datei-Upload über HTML-Formulare?

Keywords: Datei | Upload | input | POST | enctype | HTML

Antwort von Kerry W. Lothrop

Ein Upload-Formular muss ein Input-Element enthalten, das den Typ file hat. Da Dateien in einem Upload prinzipiell beliebig groß werden können, muss die Übermittlung des Formulares mit der Methode POST erfolgen. Außerdem muss ein bestimmter ENCTYPE für das Formular angegeben werden. Ein solches Formular kann von Netscape Navigator ab Version 3 und von Microsoft Internet Explorer ab Version 4 verarbeitet werden.

<h1>Hallo</h1>
<form action="/submit.php" method="post"
      enctype="multipart/form-data">
<input type="file" name="probe" />
<input type="submit" value="los" />
</form>

Das empfangende PHP-Script bekommt das Resultat des Datei-Uploads als assoziatives Array mit dem Namen $_FILES['probe'] (bzw. bei PHP 4.0.0 bis 4.0.6 $HTTP_POST_FILES['probe']) übermittelt, weil das Input-Element im Formular diesen Namen hat.

$_FILES['probe']['tmp_name']

Diese Variable enthält den Namen der Datei in einem temporären Verzeichnis auf dem Server. Sie kann von dort mit einem move_uploaded_file() -Aufruf abgeholt werden. Das ist auch notwendig, da die Originaldatei am Ende des Scriptes automatisch gelöscht wird.

$_FILES['probe']['name']

Diese Variable enthält den Namen der Datei auf dem System des Anwenders. Der genaue Dateiname mit evtl. vorhandenen Laufwerksbuchstaben, Pfadseparatoren und anderen Sonderzeichen ist betriebssystemabhängig und das empfangende Script sollte keine Annahmen hierüber machen.

$_FILES['probe']['size']

Diese Variable enthält die Länge der Datei auf dem Server in Bytes.

$_FILES['probe']['type']

Diese Variable enthält den MIME-Type der Datei, so wie er dem Server vom Browser übermittelt worden ist. Dieser Wert kann unter Umständen nicht richtig sein, je nach Einstellung des Browsers. Beim Ermitteln des Typs von hochgeladenen Grafiken sollte stattdessen die Funktion getimagesize() verwendet werden.

$_FILES['probe']['error']

Diese Variable wurde mit PHP 4.2.0 eingeführt und enthält den Status des Dateiuploads. Die möglichen Werte und dazugehörige Konstanten in neueren PHP-Versionen sind im Handbuch aufgeführt.

Der Upload von Dateien wird durch die drei Konfigurationsparameter file_uploads , upload_tmp_dir und upload_max_filesize gesteuert. Außerdem relevant sind die drei Parameter memory_limit , post_max_size und max_execution_time . Der Pfad zu upload_tmp_dir muss absolut angegeben werden.

PHP legt die temporäre Datei in dem angegebenen Verzeichnis an und löscht sie am Ende des Scriptes wieder. Die Datei darf maximal die angegebene Größe haben. Ein Einstellen der Größenbegrenzung begrenzt jedoch nicht wirklich den Plattenplatz, der auf dem Server von PHP durch Fileupload verbraucht wird: Aus technischen Gründen muss PHP die Datei zunächst empfangen und kann sie erst dann verwerfen, wenn sie zu groß ist. Es kann auch mehr als eine Datei pro Formular hochgeladen werden (siehe Wie kann ich mehrere Dateien auf einmal uploaden?).

Achtung:

Das Kopieren der hochgeladenen Datei mit Hilfe von copy() und das Verwenden der alten, auf globalen Variablen beruhenden Version sollte wo möglich aus Sicherheitsgründen vermieden werden.

Vollständiges Beispiel:

<h1>Upload</h1>

<form
  action="<?php echo $_SERVER['PHP_SELF']; ?>"
  method="post"
  enctype="multipart/form-data">
<input type="file" name="probe" />
<input type="submit" value="Los!" />
</form>
<hr />
<?php
  if (isset($_FILES['probe']) and ! $_FILES['probe']['error']) {
    // Alternativ:            and   $_FILES['probe']['size']
    move_uploaded_file($_FILES['probe']['tmp_name'], "./newfile.txt");
    printf("Die Datei %s steht jetzt als " .
          "newfile.txt zur Verfügung.<br />\n",
      $_FILES['probe']['name']);
    printf("Sie ist %u Bytes groß und vom Typ %s.<br />\n",
      $_FILES['probe']['size'], $_FILES['probe']['type']);
  }
 ?>

Bei PHP3 muss über eine alternative Syntax auf die hochgelade Datei zugegriffen werden (siehe Wie funktioniert ein Datei-Upload über HTML-Formulare bei PHP 3?).

11.12. Wie kann ich mehrere Dateien auf einmal uploaden?

Antwort von Johannes Frömter

Das Auswählen mehrerer Dateien oder gar ganzer Verzeichnisse ist mit einem <input type="file">-Feld nicht möglich. Auch das Vorgeben eines bestimmten Verzeichnisses oder vollständigen Pfades ist bei File-Input-Feldern unterbunden, sei es als Angabe value="path/to/file" oder per JavaScript. Warum? Aus Sicherheitsgründen! Wem wäre es schon recht, wenn auf einer x-beliebigen Internetseite sich ein (z.B. durch Layer verstecktes) Formular mit einem Feld <input type="file" value="c:\eigene dateien\*.*"> mittels JavaScript selbsttätig abschicken würde? Eben deshalb muss jede Datei, die verschickt werden soll, vom Anwender manuell und damit bewußt ausgewählt werden.

Mehrere Dateien lassen sich verschicken

  • mit mehreren <input type="file">-Feldern - pro Datei eines (Tipp: [] an den Namen des Input-Feldes anhängen, um in PHP ein Array mit den Dateiinformationen zu erhalten)

  • als .zip- oder .tar-Datei

  • per FTP

  • mit einem eigenen Tool, das auf dem Rechner des Absenders installiert werden muss

  • per (Java-) Applet - hier gibt es mehrere kommerzielle oder kostenlose Lösungen, z.B.

Antwort von Kerry W. Lothrop

Beim Verwenden der Array-Notierung in Zusammenhang mit mehreren Datei-Upload-Feldern ist die Zuweisung im $_FILES-Array nicht so, wie man es vermuten könnte. Beispiel:

<form
  action="<?php echo $_SERVER['PHP_SELF']; ?>"
  method="post"
  enctype="multipart/form-data">
<input type="file" name="probe[test][]" />
<input type="file" name="probe[test][]" />
<input type="submit" />
</form>
<?php
if (isset($_FILES)) {
  ?><pre><?php print_r($_FILES); ?></pre><?php
}
?>

Der Name der ersten Datei z.B. findet sich in diesem Beispiel unter $_FILES['probe']['name']['test'][0], der Fehlercode der zweiten Datei unter $_FILES['probe']['error']['test'][1].

11.13. Wie erfahre ich den Status während eines Datei-Uploads?

Antwort von Johannes Frömter

Ein Datei-Upload mittels HTML-Formular findet zwischen Browser und Webserver statt. Während der Upload im Gange ist, weiß PHP noch gar nichts davon. Erst wenn der Upload beendet ist, tritt PHP in Aktion und nimmt die Daten entgegen. Daraus folgt, dass es mit PHP/HTML/JavaScript-Mitteln (und seien sie noch so ausgefeilt) technisch unmöglich ist, den Benutzer über den tatsächlichen Fortschritt des Uploads zu informieren.

Am Sinnvollsten wäre eine Statusanzeige clientseitig, also als Browser-Feature, zu realisieren. Die derzeit erhältlichen Browser haben diese Funktion leider nicht implementiert.

11.14. Wie verarbeite ich <input type="image">?

Keywords: Formular | Submit | Image | Koordinaten

Antwort von Kristian Köhntopp

In Formularen kann man statt eines SUBMIT auch ein IMAGE als Absendeknopf installieren. Dies sieht dann so aus:

<input type="image" src="meinbild.png" name="sub" />

Wenn der User das Bild anklickt, werden zwei Variablen mit den Namen sub.x und sub.y erzeugt, die die Koordinaten des Klicks relativ zur linken, oberen Ecke des Bildes beschreiben. Da Variablennamen in PHP keine Punkte enthalten dürfen, wandelt PHP die Punkte in Unterstriche um. Im Beispiel bekommt man die Variablen mit den Namen $_REQUEST['sub_x'] und $_REQUEST['sub_y'] übergeben (bzw. vor PHP 4.1.0 $HTTP_GET_VARS['sub_x'], etc. oder $HTTP_POST_VARS['sub_x']).

Antwort von Johannes Frömter

Alternativ kann man an den Variablennamen eines <INPUT TYPE="image"> eckige Klammern [] anhängen; man erhält in PHP dann ein Array mit dem Namen des Buttons, das die Koordinaten des Klickpunktes enthält. Mehrere solcher Image-Buttons kann man als button[a][], button[b][] usw. benennen und die Werte aus den Arrays $_REQUEST['button']['a'], $_REQUEST['button']['b'] usw. auslesen. Ob ein bestimmter Button gedrückt wurde, überprüft man mit isset() : if (isset($_REQUEST['button']['b'])) (bzw. vor PHP 4.1.0 if (isset($HTTP_GET_VARS['button']['b'])) oder if (isset($HTTP_POST_VARS['button']['b']))).

11.15. Wie erkenne ich den Klick auf einen Submit-Button?

Antwort von Johannes Frömter

Der Button muss einem Namen haben:

<input type="submit" name="submit" value="OK"/>

Dann ist bei einem Mausklick oder einem Tastendruck auf den Submit-Button eine Variable mit dem Namen des Buttons vorhanden:

if (isset($_REQUEST['submit'])) { ... }
// vor PHP 4.1.0 $HTTP_GET_VARS['submit'] oder $HTTP_POST_VARS['submit']

(Die Variable enthält den Text, der bei value angegeben wurde, oder den Standard-Text des Browsers für die Schaltfläche.)

Wurde das Formular dagegen per JavaScript oder durch Drücken der Eingabetaste im einzigen Texteingabefeld des Formulars abgeschickt, ist die Variable nicht vorhanden.

Man kann diese Abfrage sehr gut bei den sog. "Affenformularen" (Formulare, die sich selbst aufrufen, siehe "Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform") gebrauchen.

11.16. Wie verarbeite ich mehrere Submit-Buttons?

Antwort von Johannes Frömter

Es gibt verschiedene Möglichkeiten, in PHP zu unterscheiden, welcher Submit-Button in einem HTML-Formular betätigt wurde:

  • Haben die Buttons den gleichen Namen (name="submit"), kann man den value (gleichzeitig Beschriftungstext des Buttons) auswerten; die PHP-Variable heißt so wie der Button ($_REQUEST['submit']).

  • Haben die Buttons unterschiedliche Namen, erhält man je nach betätigtem Button eine Variable mit anderem Namen registriert; mit isset() kann man prüfen, ob eine bestimmte Variable vorhanden ist, d.h. ob ein bestimmter Button angeklickt wurde.

  • Benennt man die Buttons in der Array-Schreibweise (name="submit[0]", zwischen den eckigen Klammern müssen eindeutige Werte stehen), erhält man in PHP ein Array mit genau einem Element; der Schlüssel (Key) dieses Elementes ist der aktivierte Button.

Im Script kann man dann z.B. unterschiedliche Anweisungsblöcke mit include() einbinden und somit ausführen.

11.17. Wie verarbeite ich einen Reset-Button?

Antwort von Johannes Frömter

Zunächst sollte man überlegen, ob man überhaupt einen Reset-Button braucht - bei Formularen mit nur einem Eingabefeld ist er eher witzlos, bei umfangreichen Formularen ist er dagegen umso ärgerlicher, wenn er aus Versehen betätigt wurde!

Bei einem HTML-Reset-Button (<input type="reset">) setzt der Browser alle Eingabefelder auf den Anfangszustand zurück; da damit kein Request an den Server verbunden ist, kriegt PHP davon nichts mit. Möchte man einen Button realisieren, der Eingabeelemente mit vordefinierten Inhalten wirklich löscht, muss man einen Submit-Button nehmen:

<?php
  if (isset($_REQUEST['loeschen'])) {
      unset($_REQUEST['eingabe']);
  }
?>

<form action="<?php echo $SERVER['PHP_SELF']; ?>" method="post">
<input type="text" name="eingabe"
       value="<?php @print $_REQUEST['eingabe']; ?>"/>
<input type="submit" name="submit" value="Absenden"/>
<input type="reset" value="Reset"/>
<input type="submit" name="loeschen" value="Löschen"/>
</form>

11.18. Wie erkenne ich fehlerhafte/fehlende Eingaben?

Antwort von Kerry W. Lothrop

Die Überprüfung von Formulareingaben ist in Hinblick auf die Sicherheit eines Skriptes ein nicht zu unterschätzender Faktor. Ausgangspunkt sollte meist das sogenannte Affenformular sein (siehe Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform).

Erkennen fehlender Eingaben

Die Länge einer Textfeldeingabe lässt sich mit der PHP-Funktion strlen() ermitteln. Leere Strings sind in PHP FALSE, d.h. durch eine Überprüfung auf Wahrheit (if ($_REQUEST['textfeldname'])) lässt sich feststellen, ob der Benutzer eine Eingabe gemacht hat.

Textfelder generell

Wenn ein Textfeld nur bestimmte Zeichen enthalten darf, ist es oft am einfachsten zu überprüfen, ob der übermittelte String Zeichen enthält, die nicht erlaubt sind (siehe auch Welche Bauelemente kommen in Regulären Ausdrücken vor?):

if (preg_match('/[^0-9a-z_.-]/i', $_REQUEST['textfeldname']))
    errmsg('Ungültige Zeichen im Textfeld.');

Zahlenfelder

Um zu überprüfen, ob ein Textfeld eine Zahl enthält, eignet sich die Funktion is_numeric() . Diese Funktion akzeptiert allerdings auch Zeichen, die nicht Ziffern sind (z.B. -1.6e-12). Sollen nur Ziffern akzeptiert werden, helfen wieder Reguläre Ausdrücke:

if (preg_match('/\D/', $_REQUEST['textfeldname']))
    errmsg('Ungültige Zeichen im Zahlenfeld.');

URLs

Zum Überprüfen einer URL reicht es meist aus, deren Anfang zu analysieren:

if (! preg_match('=(https?|ftp)://[a-z0-9]([a-z0-9-]*[a-z0-9])?\.[a-z0-9]=i', $_REQUEST['textfeldname']))
    errmsg('Ungültige URL im Adressfeld.');

Diese Überprüfung soll nur ein Ansatz sein und lässt sich beliebig erweitern/verfeinern. Die tatsächliche Erreichbarkeit gewährleistet diese Vorgehensweise allerdings nicht. Mit Hilfe der Funktion fopen() lässt sich die momentante Erreichbarkeit einer URL feststellen.

E-Mail-Adressen

Diese Frage wird unter Wie kann ich feststellen, ob eine Mailadresse äußerlich gültig ist? und Wie kann ich die Gültigkeit einer Mailadresse testen? beantwortet.

11.19. Wie verhindere ich mehrfaches Absenden eines Formulars?

Antwort von Guido Haeger

Daten die ein Script aus Formularen übergeben bekommt, sollen meist in Datenbanken gespeichert werden. Dabei sollen in der Regel Dubletten vermieden werden. Diese können z.B. entstehen, wenn der User den URI neulädt, an den die Daten übergeben wurden oder wenn er bei Netscape-Browsern die Größe des Browserfensters ändert, was ebenfalls ein Neuladen bewirkt. Ein mögliches Verfahren zur Vermeidung solcher Dubletten wird in der Frage Wie kann ich eine schummelsichere Abstimmung codieren? beschrieben (Einsatz einer Challenge), ein anderes mögliches Verfahren ist ein Redirect per Location-Header nach der erfolgreichen Ausführung der gewünschten Aktionen (z.B. Speichern der Daten in einer Datenbank).

<?PHP
function formular_ausgeben($error = '')
{
   $error = ($error=='')?'':'<font color="#DD0000">'.$error.'</font>';
   return '<form action="script.php4" method="POST">
           '.$error.'
           Name: <input type="text" name="name" value="'.$_REQUEST['name'].'" />
           <br />
           <input type="submit" name="submit" value="OK" />
           </form>';
}

function daten_speichern()
{
   GLOBAL $db;
   if(strlen($_REQUEST['name']) < 3)
   {
      return formular_ausgeben('Bitte Namen eingeben!<br>');
   }
   else
   {
      // Mit $db->escape_string() wird eine Funktion aufgerufen, die sicher stellt
      // das möglicherweise in $_REQUEST['name'] enthaltene SQL-Fragmente nicht
      // ausgeführt werden (siehe auch mysql_escape_string() ).
      $db->query("INSERT INTO tabelle
                  SET name = '".$db->escape_string($_REQUEST['name'])."'");

      if($db->affected_rows() == 1)
      {
         header('Location: http://domain.de/script.php4?d=bestaetigen');
         return '<a href="http://domain.de/script.php4?d=bestaetigen">weiter</a>';
      }
      else
      {
         return formular_ausgeben('MySQL: '.$db->Error.'<br>');
      }
   }
}

function bestaetigung()
{
   return 'Der Name wurde gespeichert.';
}


if($_REQUEST['submit'] == 'OK')
{
   echo daten_speichern();
}
elseif($_REQUEST['d'] == 'bestaetigen')
{
   echo bestaetigung();
}
else
{
   echo formular_ausgeben();
}
?>

Ein Neuladen nach erfolgreichem Abspeichern der Daten führt hier nur zur erneuten Ausgabe der Bestätigung. Die Daten werden nicht erneut in die Datenbank geschrieben.

11.20. Warum funktionieren meine Formulare nicht?

Antwort von Richard Körber

Die Symptome sind meist, dass die Seiten auf dem Webserver des Hosts tadellos funktionieren, aber bei dem nagelneu aufgesetzten Testserver die Formulareingaben einfach nicht mehr ankommen. Oft stolpern auch Neulinge darüber, wenn die Beispiele aus dem Lehrbuch nicht auf dem eigenen Testserver funktionieren.

Seit PHP 4.2.0 wurde register_globals in der php.ini per Default abgeschaltet. Die Option war dafür zuständig, dass ein Parameter an das Script automatisch in eine entsprechend genannte Variable kopiert wurde. Ein Angreifer hat damit jedoch die Möglichkeit, Variablen, die der Programmierer versehentlich uninitialisiert ließ, mit beliebigen Werten vorzubelegen.

Es gibt zwei Möglichkeiten, dieses Problem in den Griff zu kriegen.

Die gute Wahl ist, die PHP-Scripte entsprechend anzupassen. Das ist reine Fleißarbeit. Siehe auch Verwendung von Register Globals .

Die schlechte Wahl ist, register_globals wieder zu aktivieren. Da der Webauftritt damit wieder angreifbar ist, sollte man es höchstens als Notlösung betrachten, um den Webauftritt vorübergehend wieder online zu kriegen, bis man die Scripte umgestellt hat. Wenn man keine Rechte hat, um register_globals zu aktivieren, hilft die Funktion import_request_variables() weiter.

12. Sicheres Programmieren in PHP

12.1. Wie unterscheide ich böse Variablen von guten?

Keywords: Sicherheit | Variablen | GET | POST | Cookie

Antwort von Kristian Köhntopp

Es ist im wesentlichen egal, wie die Daten in das lokale Programm kommen - GET, POST, COOKIE oder Request-Parameter spielt kaum eine Rolle. Es ist jedoch wesentlich, dass diese Variablen a) keine Variablen des lokalen Programmes unkontrolliert überschreiben können und b) dass der Übergang von solchen benutzerkontrollierten Variablen in lokale Variablen des Programmes nur nach einer Prüfung der Variablen auf legale Werte stattfindet. Das bedeutet:

  1. Variablen mit vorgegebenen Werten aus einer Wertemenge ("Auswahlvariablen"), wie sie zum Beispiel aus einem <select> fallen:

    Das Programm sollte ein lokales Array haben, das die möglichen Werte als Index eines Hash enthält:

    $val = array(
            "wert1" => 1,
            "wert2" => 1,
            "wert3" => 1
           );
    

    Die Prüfung von $_GET["selectvar"] kann nun so aussehen:

    $checkedvar = isset($val[$_GET["selectvar"]]) ? $_GET["selectvar"] : "default";
    

    Wenn $_GET["selectvar"] einen in $val definierten Wert hat, dann wird dieser Wert nach $checkedvar übernommen. Hat man versucht, dem Programm einen vierten Wert unterzuschieben, dann wird $checkedvar auf "default" gesetzt. Das Programm arbeitet dann ausschliesslich mit $checkedvar. Auf diese Weise können dem Programm keine beliebigen Werte aus $_GET["selectvar"] untergeschoben werden.

  2. Variablen, die eine bestimmte Form haben müssen, aber für die nicht die abgeschlossene Wertemenge aufgezählt werden kann ("Freitextvariablen mit Formvorschrift"):

    Das Programm sollte eine oder mehrere Regex in einem Feld vorliegen haben:

    $val = array(
                 '/^[a-zA-Z ]+$/',
                 '/^[0-9-]+$/'
                );
    

    Die Prüfung kann nun so aussehen:

    $checkedvar = "default";
    if (isset($_GET["inputvar"])) {
      foreach ($val as $k => $v) {
        if (preg_match($v, $_GET["inputvar"])) {
          $checkedvar = $_GET["inputvar"];
          break;
        }
      }
    }
    

    Wenn $_GET["inputvar"] einen Wert hat, der auf eines der Muster in $val passt, wird dieser Wert in $checkedvar übernommen. Dazu müssen die Muster in $val natürlich vorne und hinten verankert sein (also mit ^ beginnen und $ enden), sonst kann man der Prüfung etwas unterschmuggeln. Paßt kein Muster, hat $checkedvar hinterher den Wert "default". Das Programm arbeitet dann ausschliesslich mit $checkedvar. Auf diese Weise können dem Programm keine beliebigen Werte aus $_GET["selectvar"] untergeschoben werden.

  3. Variablen, die eine numerische Form haben müssen.

    Numerische Variablen können durch eine einfache Konvertierung erzwungen werden. Danach muss eine Überpruefung des Wertebereiches erfolgen:

    $checkedvar = isset($_GET["numericvar"]) ? $_GET["numericvar"]+0 : 0;
    

    Durch die Addition von 0 wird eine Konvertierung auf Integer erzwungen. Der Wert ist hinterher entweder Wert des numerischen Prefix von $_GET["numericvar"] oder 0. "123abc" wird also zu 123, "fasel" zu 0. Ein fehlender Wert wird durch das isset() ebenfalls zu 0.

  4. Variablen, die Dateinamennatur haben.

    Programme sollen oft Dateinamen, aber keine Pfadnamen annehmen. Eine Prüfung auf "/" im Dateinamen reicht oftmals als Sicherheitsprüfung aus. Zwingt man außerdem noch die Endung hinten dran, erschwert man die Ausnutzung von Sicherheitslücken zusätzlich, sollten dennoch welche vorhanden sein.

    $checkedvar = "default";
    if (isset($_GET["filenamevar"]) 
       && !preg_match('=/=', $_GET["filenamevar"])) {
       $checkedvar = $_GET["filenamevar"] . ".ext";
    }
    
  5. Templatenamen

    Templatenamen sind kein Spezialfall von Dateinamen oder Namen mit Formvorschrift, sondern ein Spezialfall von vorgegebenen Werten aus einer Liste und sollten wie der erste Fall behandelt werden. Danke.

  6. Benutzerinput, der Tags enthalten kann

    Cross Site Scripting Attacks sind Attacken, in denen eine Benutzereingabe aus einem Formular in die eigene Site als HTML, CSS oder anderer Formattext übernommen wird. Auf diese Weise kann der Benutzer eigene Elemente in unsere Website einbinden (etwa Bilder, Formulare oder Javascript von seiner Site), die einem Opfer dann als legitime Bestandteile unserer Site erscheinen und mit den Rechten unserer Site beim Opfer ausgeführt werden.

    strip_tags() auf GRUNDSÄTZLICH ALLE Benutzereingaben ist eine ausgesprochen gute Idee. Das schließt gegebenenfalls den Referer und andere benutzerkontrollierte Variablen mit ein! So existiert zum Beispiel eine Attacke gegen webalizer, bei der man einer Site Referer unterschiebt, die HTML enthalten. In der webalizer-generierten Referer-Statistik der Site erscheint dann feindseliges HTML!

12.2. Was genau bewirkt safe_mode und ist das sicher?

Antwort von Kristian Köhntopp

Die hier aufgeführte Liste basiert auf einer Analyse einer bestimmten, inzwischen veralteten Version des PHP-Sourcecodes. Die grundsätzlichen Informationen sind korrekt, aber die Liste der einzelnen Teileinschränkungen dürfte nicht mehr abschließend sein.

Es gibt eine Konfigurationsvariable safe_mode , die in der php.ini gesetzt werden kann. Weiterhin gibt es die Konfigurationsvariablen safe_mode_exec_dir und sql_safe_mode .

Wenn safe_mode aktiv ist, sind verschiedene PHP-Funktionen privilegiert oder eingeschränkt. Zumeist gilt die safe_mode Einschränkung, dass auf eine Datei oder ein Verzeichnis nur eingewirkt werden darf, wenn die Datei oder das Verzeichnis denselben Eigentümer hat wie das Script. Im Einzelnen:

  • Alle Dateifunktionen einschließlich include() und require() können nur noch mit lokalen Dateien arbeiten, die denselben Eigentümer (uid) haben wie der Eigentümer des Scriptes.

  • Lokalen, absoluten Pfadnamen wird document_root vorangestellt.

  • In den Funktionen popen() , system() und exec() können nur Kommandos ausgeführt werden, die im safe_mode_exec_dir stehen. Auf die ausgeführten Kommandos wird von den Funktionen popen() , system() und exec() automatisch EscapeShellCmd() angewendet.

  • Dynamisch ladbare Erweiterungen ( dl() -Funktion) stehen in safe_mode nicht zur Verfügung ("Dynamically loaded extensions aren't allowed when running in SAFE MODE.").

  • Es kann kein neues CPU-Zeitlimit festgelegt werden ("Cannot set time limit in safe mode").

  • Wird mod_php verwendet, steht in der Funktion getallheaders() der Wert des Headers "Authorization" (Loginname und Passwort) nicht zur Verfuegung.

  • Die Funktion header() wird dem Header "WWW-authenticate" immer den Eigentümer des Scriptes als Bestandteil des Realms hinzufügen.

  • Das filepro Map-File "dir/map" (wobei dir der Parameter der filepro()-Funktionen ist) muss die Einschränkung erfüllen. Dies gilt für filepro() , filepro_rowcount() und filepro_retrieve() .

  • Das dbase-Modul verlangt in dbase_open() und dbase_create() , dass die dbf-Datei die Einschränkung erfüllt.

  • Das DBM-Modul setzt voraus, dass in dbmopen() das Verzeichnis, in dem die DBM-Datenbank angelegt wird, die Einschränkung erfüllt. Dies ist bei den DBA-Funktionen nicht der Fall.

Wenn sql_safe_mode aktiv ist, können bei einem MySQL-Connect host, user und password nicht angegeben werden ("SQL safe mode in effect - ignoring host/user/password information").

safe_mode ist nicht sicher: Ein Fehler in der popen() -Funktion ist erst mit 3.0.14 korrigiert worden, ein weiterer Fehler in der mail() -Funktion erst in 3.0.15. Spätere Versionen von PHP hatten weitere Lücken. Man sollte stattdessen die CGI-Version in einem chroot-Environment verwenden und mit setrlimit noch weitergehende Einschränkungen definieren.

12.3. Warum ist es schlecht, mit dem Referer zu arbeiten?

Keywords: Referer | HTTP | Browser

Antwort von Martin Jansen

Der Referer ist eine Variable, in der stehen soll, von welcher Seite der Benutzer kommt, der sich gerade auf der Seite befindet. Hat der User zum Beispiel bei einer Suchmaschine auf einen Link geklickt, so würde der Referer "http://suchmaschine.tld/query?suchwort=german-faq" lauten.

Dies muss allerdings nicht immer so sein:

  • Die Entwickler des Browsers haben nicht vorgesehen, dass ein Referer-String mitgesendet wird oder der User hat das Mitsenden des Referers im Browser deaktivert.

  • Der User verwendet einen lokalen Proxyserver (z.B. Webwasher), der die Referer-Information herausfiltert.

  • Ein Proxyserver bei einem Provider, im Rechenzentrum einer Universität oder in einem Unternehmen ist so konfiguriert, dass er keine Referer-Strings mitsendet.

  • Neben dem kompletten Entfernen des Referer-Strings aus den Headerdaten kann es auch möglich sein, dass der Referer durch o.g. Quellen modifiziert wird und dadurch unbrauchbar wird.

Diese Punkte sind Argumente dafür, den Referer nicht zu sicherheitsrelevanten Zwecken auf einer Website einzusetzen.

12.4. Für welche Zwecke ist der Referer zu gebrauchen?

Antwort von Johannes Frömter

Für Webserver-Statistiken zum Beispiel. Im Standard-Format des access.log von Apache wird der HTTP-Referer erfasst (wenn der Browser einen liefert); bereiten Statistikprogramme diese Logfiles auf, können sie Aussagen über Klickpfade machen, z.B.:

  • Zugriffe: "Von welcher meiner Seiten wird am Häufigsten auf Seite xy gegangen?"

  • Einstiegsseite: "Auf welche Seiten kommen Besucher von außen häufig?"

  • Linkseiten: "Welche andere Seiten linken mich?"

  • Fehlerhafte Links: "Von welcher Seite kam der Besucher zu dem toten Link?"

Will man den Referer trotz der in Warum ist es schlecht, mit dem Referer zu arbeiten? beschriebenen Gegenargumente für weitergehende Zwecke benutzen (z.B. zum Schutz eines Programmdownloades vor direkter Verlinkung), sollte man unbedingt so vorgehen, dass Requests mit fehlendem Referer nicht behindert werden. Der Besucher kann völlig schuldlos an einem fehlenden Referer sein, z.B. wenn er hinter einem Proxy in der Firma sitzt. Daher darf man den Zugriff nur dann abblocken, wenn der Referer vorhanden ist, aber einen nicht erlaubten Wert hat. Bei der umgekehrten Vorgehensweise (Zugriff nur bei Referer von der eigenen Seite) verscherzt man es sich unweigerlich mit einem Teil seiner Besucher.

In PHP hat man über die Variable $_SERVER["HTTP_REFERER"] Zugriff auf den Referer. Die Funktion parse_url() dürfte beim Zerlegen des Strings sehr hilfreich sein, damit man ihn einfacher vergleichen kann.

Man sollte sich aber immer bewusst sein, dass der Referer keine 100%-ige Sache ist. Will (oder muss) man den Zugriff auf bestimmte Ressourcen "wasserdicht" gestalten, führt kein Weg an einer Session-Lösung vorbei.

12.5. Kann ich PHP-Dateien kompilieren und so vor Dritten schützen?

Antwort von Johannes Frömter

Anders als beispielsweise bei Java wird PHP-Code erst zur Laufzeit (wenn das Script aufgerufen wird) kompiliert, d.h. der Programmcode liegt im "Klartext" in den .php-Dateien. Für normale Besucher einer von PHP erzeugten Webseite ist der Programmcode nicht einzusehen. Solange man die Scripte nur auf dem eigenen Webserver einsetzt, ist der Code für andere unsichtbar (siehe hierzu auch den Artikel 'Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?'). Gibt man PHP-Dateien jedoch (an Kunden) weiter, erhalten diese damit den Source Code. Wenn der Code nicht "Open Source" ist, sollte man per Vertrag das Copyright und die Nutzungsbedingungen genau regeln.

Von Zend gibt es als kommerzielles Produkt den Zend Encoder Unlimited, der PHP-Code in ein binäres Format überführt und damit weitgehend vor Ausspähung, Veränderung und Re-engineering schützt. Das Produkt ist nicht gerade billig, außerdem braucht man natürlich auf jedem Webserver, der kompilierten Code ausführen soll, ein Zusatzmodul (dieses allerdings ist kostenlos).

Nach dem gleichen Prinzip funktioniert der PHP Bytecode Compiler, der im derzeitigen Beta-Stadium allerdings noch keinen objektorientierten Code verarbeiten kann. Die Kompilierung erfolgt außerdem "online", der Encoder ist (noch) nicht zum Download verfügbar.

Auch microCODE macht aus PHP-Code einen nicht lesbaren Bytecode, der durch ein einzubindendes .so-Modul vom PHP-Interpreter ausgeführt werden kann.

Einen etwas anderen Weg gehen die Tools Code Obfuscator und POBS - sie kommen ohne serverseitige Module aus, da sie den Code nur für Menschen, nicht aber für den PHP-Interpreter "unleserlich" machen. Der Code Obfuscator ersetzt die (meist sprechenden) Variablennamen durch (nichtssagende) Konstrukte, so dass es schwer ist, die Funktion eines so bearbeiteten Scripts nachzuverfolgen. POBS macht dasselbe, geht aber noch weiter, indem auch Konstanten und Funktionsnamen verändert und Kommentare, Einrückungen sowie Leerzeilen entfernt werden. Der resultierende Code ist immer noch gültiger PHP-Code, aber seine Funktionsweise ist kaum mehr zu entziffern.

12.6. Apache: Wie kann ich ein Verzeichnis mit einem Passwort schützen?

Antwort von Kristian Köhntopp

Die Konfiguration des Webservers ist auch noch einmal ausführlich im Handbuch zum Apache Webserver beschrieben. Philipp Imhof hat auf seinem Webserver ein Script installiert, dass eine an den jeweiligen Beispielfall angepasste Anleitung erstellt.

Im einfachsten Fall verwendet man HTTP Basic Authentication. Dazu ist mit dem Apache-Programm htpasswd eine Passwortdatei anzulegen, die die Benutzernamen und Passworte enthält. Optional kann man auch eine Gruppendatei anlegen, die pro Zeile den Namen einer Benutzergruppe und, durch einen Doppelpunkt getrennt, die Benutzer in dieser Gruppe enthält.

kris@valiant:~/www/kris.koehntopp.de > htpasswd -c etc/htpasswd kris
New password: test
Re-type new password: test
Adding password for user kris
kris@valiant:~/www/kris.koehntopp.de > htpasswd etc/htpasswd marit
New password: test
Re-type new password: test
Adding password for user marit
kris@valiant:~/www/kris.koehntopp.de > echo "users: kris marit" >>
etc/htgroup

Die Option -c steht dabei für create; die Passwortdatei wird neu angelegt. Ohne die Option werden Benutzer zu einer existierenden Passwortdatei zugefügt bzw. die Passworte existierender Benutzer werden geändert. Im Beispiel wird weiterhin eine Gruppe users angelegt, der die Benutzer kris und marit angehören.

Ein Verzeichnis kann man nun durch das Anlegen einer .htaccess-Datei schützen (dazu muss AllowOverride AuthConfig in der httpd.conf für dieses Verzeichnis gesetzt sein) oder indem man die entsprechenden Konfigurationsanweisungen direkt in einen <Directory>-Block für dieses Verzeichnis in die httpd.conf einsetzt. Im einfachsten Fall sind dies die Anweisungen:

AuthType Basic
AuthName MyRealm
AuthUserFile /home/www/servers/www.koehntopp.de/etc/htpasswd
AuthGroupFile /home/www/servers/www.koehntopp.de/etc/htgroup

require valid-user

Die Anweisung AuthType legt fest, auf welche Weise der Browser das Passwort übermittelt - bei der sehr unsicheren Basic-Authentication wird es nahezu im Klartext übertragen. Für den zu schützenden Bereich muss mit AuthName ein Name festgelegt werden. Die Direktiven AuthUserFile/ und AuthGroupFile legen die Datenquellen für die Authentisierung fest.

Damit ein Anwender den geschützten Bereich betreten kann, muss er die in der require-Anweisung geforderten Bedingungen erfüllen. Im einfachsten Fall muss er einfach nur in der htpasswd-Datei stehen und das passende Passwort wissen. Dies ist der Fall bei require valid-user. Mit komplizierteren Anweisungen kann man den Zutritt auch noch auf bestimmte Gruppen (require group users) oder auf bestimmte Benutzer (require user kris) einschränken.

In CGI PHP hat man in einem auf diese Weise geschützten Bereich Zugriff auf den Benutzernamen in der Variablen $_SERVER["REMOTE_USER"] und die verwendete Authentisierungsmethode in $_SERVER["AUTH_TYPE"].

<?php
echo "AUTH_TYPE = ", $_SERVER['AUTH_TYPE'], "<br />\n";
echo "REMOTE_USER = ", $_SERVER['REMOTE_USER'], "<br />\n";
?>

Im Apache-Modul hat man in einem geschützten Bereich außerdem Zugriff auf die vollen Authentisierungsdaten.

<?php
echo "AUTH_TYPE = ", $_SERVER['AUTH_TYPE'], "<br />\n";
echo "REMOTE_USER = ", $_SERVER['$REMOTE_USER'], "<br />\n";
$a = getallheaders();
$au = split(" ", $a["Authorization"], 2);
list($u, $p) = split(":", base64_decode($au[1]));
echo "Decodiert zu User $u, Password $p<br />\n";

12.7. Apache: Wie kann ich ein Verzeichnis mit PHP mit einem Passwort schützen?

Antwort von Kristian Köhntopp

Dies ist in englischer Sprache ausführlich im PHP Handbuch beschrieben. Das Feature steht nur dann zur Verfügung, wenn PHP als Apache-Modul betrieben wird.

<?php
function check_pw($u, $p) {
  ...
}

if (!isset($_SERVER['PHP_AUTH_USER']) 
 or !check_pw($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']))
{
    Header("WWW-Authenticate: Basic realm=\"My Realm\"");
    Header("HTTP/1.0 401 Unauthorized");
    echo "Text to send if user hits Cancel button\n";
    exit;
} else {
    echo "Hello $PHP_AUTH_USER.<P>";
    echo "You entered $PHP_AUTH_PW as your password.<P>";
}
?>

Nur wenn PHP selbst die Authentisierung vornimmt, stehen die Variablen PHP_AUTH_USER und PHP_AUTH_PW zur Verfügung. Sie können verwendet werden, um den Benutzer in einer Datei, einer Datenbank oder einer anderen Datenquelle nachzuschlagen und das Passwort zu überprüfen.

12.8. Kann ich mit CGI PHP ein Verzeichnis mit einem Passwort schützen?

Antwort von Kristian Köhntopp

Nicht mit den Bordmitteln von PHP. Zwar kann man durch Senden eines Status-Header die Authentisierung auslösen, aber PHP übermittelt nicht die notwendigen Variablen an das Script zurück, wenn der Benutzer sich angemeldet hat.

Stattdessen sollte man Sessions verwenden und sich selbst ein Login basteln.

12.9. Wie kann ich Passwörter sicher speichern?

Antwort von Alex Kiesel

Um Passwörter nicht im Klartext in eine Datenbank speichern zu müssen, gibt es die Möglichkeit diese vorher mit einer Einweg-Verschlüsselung zu versehen, bzw. nur einen Hash des Passworts abzuspeichern. Die Funktionen crypt() und md5() können hierfür benutzt werden.

Crypt verschlüsselt auf den meisten Systemen das Passwort mit DES, es werden jedoch nur die ersten 8 Zeichen zur Verschlüsselung benutzt, längere Passwörter werden also einfach abgeschnitten.

MD5 ist eine kryptographische Hash-Funktion. Sie berechnet für einen beliebigen Eingabestring einen Hash, der 128 Bit lang ist.

Beide Funktionen haben Eigenschaften, die uns hier sehr nützlich sind:

  • Sie sind determiniert: sie geben für eine bestimmte Eingabe immer das gleiche Ergebnis zurück.

  • Es lässt sich nicht vom Ergebnis auf die Eingabe schliessen. Damit ist eine "Rückrechnung" des Passwort-Hashes auf das echte Passwort des Benutzers nicht möglich (außer durch Brute-Force).

  • Es ist sehr unwahrscheinlich (wenn auch nicht unmöglich), dass zwei verschiedene Eingaben genau denselben Hash-Wert ergeben.

Um die Passwörter der Benutzer also sicher zu speichern, wird vor dem Speichern in z.B. einer Datenbank erst der Hash des Passworts berechnet und dieser dann gespeichert. Da wir (und vor allem andere) nicht in der Lage sind, aus dem gespeicherten String das wirkliche Passwort zu berechnen, kann bei der Prüfung der Passworteingabe nicht das Passwort direkt verglichen werden. Wir berechnen zunächst also wieder den Hash der Eingabe und vergleichen diesen mit dem gespeicherten Hash. Wegen der o.g. Eigenschaft wird der Hash der Eingabe (i.A.) genau dann gleich dem gespeicherten Hashwert sein, wenn die ursprünglich eingegebenen Passwörter gleich sind.

Auch wenn die Passwörter nun sicher in der Datenbank gespeichert sind, sollte hier nicht übersehen werden, dass die Eingaben des Benutzers, also auch das Passwort, bei einer normalen HTTP-Übertragung unverschlüsselt über die Leitung gehen und somit nicht sicher sind. Eine mögliche clientseitige Verschlüsselung z.B. durch Javascript, bei der nur das Passwort verschlüsselt wird, ist keine Abhilfe dagegen. Hier würde zwar das Passwort nicht ersichtlich sein, zum Anmelden würde es aber reichen, den mitgesnifften Hash an den Server zu schicken. Letztlich bleibt für eine sichere Authentifizierung nur die Übertragung über HTTPS.

12.10. Vermeide globale Variablen

Keywords: Namensraum | GET | POST | Cookie | Parameter

Antwort von Kristian Köhntopp

In veralteten Versionen von PHP wurden eine ganze Menge Daten aus der Prozessumgebung und aus dem Internet in den globalen Namensraum importiert. Das war sehr bequem, aber auch ein Quell von zahlreichen Exploits in vielen weit verbreiteten PHP-Scripten. Mit der Version 4.2 wurde das Standardverhalten von PHP geändert: Diese Informationen stehen jetzt nur noch in besonderen, superglobalen Arrays zur Verfügung (siehe Welche Variablenarten gibt es in PHP und wie greife ich auf sie zu? und Warum funktionieren meine Formulare nicht?).

Versionen von PHP 4.2 und höher zeigen ebenfalls dieses obsolete Verhalten, sobald die Konfigurationsvariable register_globals wieder auf den Wert On gestellt wird. Es ist dringend empfohlen, diese Variable auf Off zu lassen.

Daher war es in diesen veralteten Versionen von PHP dringend notwendig, globale Variablen möglichst zu vermeiden, um Sicherheitsproblemen aus dem Weg zu gehen.

Dieser Abschnitt enthielt eine Reihe von Informationen darüber, nach welchen Regeln PHP den globalen Namensraum mit externen Daten vermatschte. Diese Informationen sind nun glücklicherweise obsolet, und wurden daher gestrichen.

Die Empfehlung, möglichst keine globalen Variablen zu verwenden bleibt jedoch bestehen: Der Namensraum von Funktionen und Klassen ist wesentlich besser kontrollierbar. Es ist daher empfehlenswert, so viel Funktionalität als möglich in Funktionen oder Klassen abzulegen und dort kontrolliert Variablen als Funktionsparameter zu importieren.

12.11. Prüfe importierte Parameter. Traue niemandem

Antwort von Kristian Köhntopp

Parameter, die als GET, POST oder COOKIE-Werte in das lokale Programm gekommen sind, sind nicht vertrauenswürdig. Techniken, die Variablen als GET-Parameter am Ende einer URL durchschleifen oder als HIDDEN-Variablen in Formularen weitergeben (Ping-Pong-Technik), sind daher prinzipbedingt fehlerhaft und können niemals sicher sein.

Auch dürfen Variablen aus GPC-Quellen niemals ungeprüft direkt verwendet werden, sondern müssen zwingend erst einmal auf Ungefährlichkeit geprüft werden (siehe Wie unterscheide ich böse Variablen von guten?). Bei Parametern, die gegen die Empfehlung per Ping-Pong weitergereicht werden, muss dieser Test jedesmal (auf jeder Seite) wieder gemacht werden, da ja ein Anwender oder ein böswilliges Programm die Werte inzwischen geändert haben könnte.

Eine Webanwendung kontrolliert nur den Bereich bis zur Firewall. Der Browser des Anwenders befindet sich auf der anderen Seite der Firewall und ist daher als Feindesland anzusehen. Daten, die von dort kommen, können unerwartete Werte oder unerwartete Formate haben. Insbesondere kann eine Webanwendung keine Annahmen über bestimmte Eigenschaften des Browsers machen, wie zum Beispiel "JavaScript wird zuverlässig ausgeführt", "Cookies werden akzeptiert und verändern ihre Werte nicht spontan" oder "Referer-Header sind vertrauenswürdig".

Es gelten daher die folgenden Empfehlungen:

  • Jede Form von Ping-Pong, also Wert-Weitergabe durch GET-Parameter, HIDDEN-Variablen und dergleichen ist zu vermeiden. Auch durch Codierung der Werte ist hier nichts zu erreichen.

    Stattdessen sind Sessionvariablen zu verwenden. Nur die Session-ID wird von einer Seite an eine andere Seite weitergereicht.

  • Keinesfalls darf ein Programm Werte aus einer GET, POST oder COOKIE-Quelle direkt verwenden. Jeder externe Wert ist einer Plausibilitätsprüfung zu unterziehen, bevor er verwendet wird (Genau das wird in Wie unterscheide ich böse Variablen von guten? näher beschrieben).

  • Validierung von Eingabewerten muss serverseitig durch PHP geschehen. JavaScript-Validatoren sind nicht vertrauenswürdig: Der Browser des Anwenders führt diese Validatoren möglicherweise nicht aus, auch dann, wenn er sich durch die User-Agent-Zeile als JavaScript-fähiger Browser identifiziert. Ebenso muss angenommen werden, dass die Referer-Header des Browsers möglicherweise gefälscht sind. Man kann nicht annehmen, dass ein Zugriff tatsächlich von einer bestimmten vorhergehenden Seite hierher vermittelt wurde.

Zusammenfassend: Traue niemandem. Validiere allen Input oder stirb.

12.12. Was sind Race Conditions? Wie kann ich sie vermeiden?

Antwort von Frank Wiegand

Wenn das Ergebnis mehrerer Ereignisse von deren Reihenfolge abhängt, und diese Reihenfolge zeitlich nicht garantiert werden kann, dann spricht man von einer Race Condition. Der Name "Race Condition" leitet sich davon ab, dass mehrere Prozesse eine Art Rennen um die betroffene Ressource austragen, welches nur einer gewinnen kann.

Ein typisches Beispiel für eine Race Condition:

<?php

  $datei = '/home/frank/datei';

  // wenn $datei nicht existiert ...
  if (!file_exists ($datei)) {
    // ... dann $datei zum Schreiben öffnen
    $fp = fopen ($datei, 'w');

    // andere Zugriffe abblocken
    flock ($fp, LOCK_EX);
    
    // Daten in die Datei schreiben
    fputs ($fp, $data);

    // Datei freigeben
    flock ($fp, LOCK_UN);
  }
  else {
    // Datei existiert bereits
  }
  
?>

Nachdem der Test mit file_exists() von dem Script (Prozess 1) durchgeführt worden ist, kann es sein, dass der Prozessor zunächst einem anderen Prozess (Prozess 2) Rechenzeit gewährt. Der Vorgang Test auf Existenz und Lockfile anlegen läuft nicht atomar, sondern geteilt ab. Prozess 2 kann in der Zwischenzeit die Datei $datei als symbolischen Link auf eine andere Datei anlegen. Das ist besonders gefährlich, da Prozess 2 beispielsweise nicht unbedingt Schreibrechte an der gelinkten Datei haben muss. Wenn Prozess 1 wieder an der Reihe ist, wird die von Prozess 2 verlinkte Datei zum Schreiben geöffnet. Ein Eindringling kann dies ausnutzen und mithilfe von Prozess 1 wichtige Daten überschreiben lassen. Oft hört man das Argument, dass dieser Fall äußerst unwahrscheinlich ist. Das stimmt nicht - oft reicht es schon aus, die entsprechenden Programme häufig (mehrere Male pro Sekunde) auszuführen. Es ist nur eine Frage der Zeit, wann die Race Condition auftritt.

Die Race Condition wird vermieden, indem man die Operation des Lockfile anlegen atomar, d. h. unteilbar ausführt. Dazu werden die Daten zunächst in eine temporäre Datei geschrieben. In PHP gibt es die Funktion tempnam() , welche eine temporäre Datei mit eindeutigem Namen anlegt. Anschliessend erstellt man die gewünschte mit der Funktion link() an der entsprechenden Stelle. link() ist eine atomare Funktion und gibt false zurück, wenn die Datei nicht erstellt werden konnte. Dabei ist zu beachten, dass link() nicht über Dateisysteme hinweg funktioniert. Die temporäre Datei muss also im selben Dateisystem (auf derselben Partition) angelegt werden.

Hier nun das Beispiel ohne Race Condition:

<?php
  
  $datei = '/home/frank/datei';
  
  // temporäre Datei anlegen
  $tempnam = tempnam ('/home/frank/tmp', 'foo');

  // Fehler: Datei konnte nicht angelegt werden
  if ($tempnam === false)
    die ("Could not create $tempnam!");
  
  // temporäre Datei zum Schreiben öffnen
  $fp = fopen ($tempnam, 'w') or die ("Could not open $tempnam!");

  // andere Zugriffe abblocken
  flock ($fp, LOCK_EX);

  // Daten in die Datei schreiben
  fputs ($fp, $data);

  // Datei freigeben
  flock ($fp, LOCK_UN);
  fclose ($fp);

  // $datei zu temporärer Datei hardlinken:
  // schlägt fehl, wenn $datei schon vorhanden ist
  if (!link ($tempnam, $datei)) {
    // Fehlschlag: Datei bereits vorhanden
  }

  // temporäre Datei löschen
  unlink ($tempnam);

?>

Unter Windows existiert die Funktion link() nicht. Stattdessen kann man rename() verwenden. Mit der Verwendung von rename() entfällt auch das Löschen der temporären Datei. rename() unter Windows überschreibt nicht vorhandene Dateien, während rename() unter UNIX/Linux dies tut.

Allgemein gilt also: Atomare Operationen wie z. B. Operationen auf Dateien müssen atomar (d. h. in einem Schritt) ausgeführt werden. Viele Funktionen geben einen booleschen Wert zurück, mit dem man überprüfen kann, ob die Operation erfolgreich war.

Diese Funktionen sind ein Anzeichen dafür, dass man eventuell eine Race Condition programmiert hat:

13. Dateifunktionen und Programmausführung

13.1. Wie kann ich eine Datei auslesen?

Keywords: Datei | Inhalt | oeffnen | Zeile | lesen | safe_mode

Antwort von Kristian Köhntopp

Man kann eine Datei manuell öffnen und Zeile für Zeile lesen:

$fp = @fopen("datei", "r") or die ("Kann Datei nicht lesen.");
while ($line = fgets($fp, 1024)) {
  machwas($line);
}
fclose($fp);

Dies verwendet die Funktionen fopen() und fgets() . Wenn die gelesenen Zeilen sofort ausgegeben werden sollen, dann kann man dies kürzer mit fpassthru() oder gar readfile() schreiben:

$fp = @fopen("datei", "r") or die ("Kann Datei nicht lesen.");
fpassthru($fp);
// fclose($fp); entfällt.

// Noch einfacher ist es mit readfile():
readfile("datei");

Will man stattdessen die Daten in der Datei in einem Array zur Verfügung haben, kann man file() verwenden. Will man die Daten in der Datei in einem einzigen String zur Verfügung haben, muss man dies mit implode() kombinieren:

// Einlesen in Array
$avar = file("datei");

// Einlesen in String
$str = implode("", file("datei"));

// Mit unterdrückten Meldungen
$str = implode("", @file("datei"));

Ab PHP 4.3.0 kann man zum Einlesen in einen String auch die Funktion file_get_contents() verwenden. Große Dateien sollte man allerdings häppchenweise mit fopen() und fread() abarbeiten.

In jedem Fall kann man den Funktionen wie üblich einen Klammeraffen voranstellen, um die Fehlermeldungen zu unterdrücken. Die häufigste Fehlermeldung bei fopen() & Co. ist "Warning: Supplied argument is not a valid File-Handle resource".

Im safe_mode unterliegt das Lesen und Schreiben von Dateien weiteren Einschränkungen.

13.2. Wie kann ich ein externes Programm von PHP aus starten?

Keywords: Programm | ausfuehren | extern

Antwort von Kristian Köhntopp

PHP kennt nicht weniger als fünf Mechanismen, um externe Kommandos (z. B. Unix-Shellbefehle) von PHP aus zu starten. Alle diese Mechanismen können zu einem Sicherheitsrisiko werden, wenn man Benutzereingaben Bestandteil der ausgeführten Kommandos oder Dateinamen werden lässt.

Durch Anwendung der Funktion EscapeShellCmd() kann man das Risiko etwas vermindern (etwa: system(escapeshellcmd($cmd))). Dennoch empfiehlt es sich, die Parameter, die in die Gestaltung von $cmd eingehen, sorgfältig zu prüfen.

Externe Kommandos werden bei Verwendung von CGI PHP unter der Identität des CGI-Wrappers ausgeführt, bei Verwendung einer Modulversion von PHP mit der Identität des Webservers (siehe auch Webserver verstehen und tunen von Kristian Köhntopp).

Will man ein externes Kommando einfach nur ausführen, kann man das betreffende Kommando einfach in Backticks setzen:

  `touch yyy`;

Dies wird im selben Verzeichnis wie das PHP-Script die Datei yyy erzeugen, falls der Webserver dort Schreibrecht hat.

Alternativ kann man ein Kommando durch die Funktion exec() starten lassen. Auch hier ist die Ausgabe nicht sichtbar, kann aber in einem Array abgelegt werden:

  exec("cat /etc/group", $lines, $result);
  echo "result = $result<br>";

  echo "Lines<br>\n";
  foreach ($lines as $k => $v) {
    echo "k=$k v=$v<br>\n";
  }

Die Ausgabe des Befehls wird im Feld $lines zur Verfügung gestellt, der Exitcode des Befehls in $result.

Die Funktion system() gibt die Ausgabe des Unix-Kommandos dagegen an den Webserver weiter. Ebenso passthru() :

  system("ls -l", $result);
  echo "Result: $result<br>\n";

  passthru("ls -l", $result);
  echo "Result: $result<br>\n";

Schließlich kann man externe Kommandos noch mit Hilfe der Funktion popen() starten:

  $fp = popen("ls -l", "r");
  while($line = fgets($fp, 1024)) {
    printf("%s<br>\n", $line);
  }

Im safe_mode unterliegt die Programmausführung weiteren Einschränkungen.

Antwort von Johannes Frömter

PHP wartet bei der Ausführung externer Programme auf deren Beendigung, d. h. das PHP-Script ist solange blockiert, bis das aufgerufene Programm fertig ist. Um dies zu vermeiden, muss man den Output des Programms umleiten, z. B. nach /dev/null:

exec("programm >/dev/null 2>&1");

Antwort von Frank Wiegand

Sollte die Ausführung des externen Programmes fehlschlagen, dann muss man diese Punkte überprüfen:

  • Enthalten das Kommando oder die Argumente shellspezifische Sonderzeichen? Diese Zeichen haben in Shells u. U. eine besondere Bedeutung und müssen entsprechend escaped werden: &, ;, `, ', ", |, *, ?, ~, <, >, ^, (, ), [, ], {, }, $ und \. Hilfreich sind hierbei die Funktionen escapeshellcmd() und escapeshellarg() .

  • Stimmt der Pfad, falls der Programmname nicht absolut gesetzt ist? Da PHP oft unter einer anderen Benutzer-ID als der eigenen läuft, muss man damit rechnen, dass die Umgebungsvariable $PATH (in PHP: $_ENV['PATH']) anders gesetzt ist. Die absolute Angabe des Pfades löst dieses Problem.

  • Hat der Benutzer, unter dem PHP läuft, Ausführungsrechte für das Programm? Herausfinden läßt sich das, indem man in einer Shell die Rechte mit ls -l /pfad/zum/kommando überprüft (das x-Bit muss entsprechend gesetzt sein). Alternativ kann man auch die Funktion is_executable() (funktioniert nicht unter Windows!) benutzen.

Hilfreich bei der Fehlersuche ist die Umleitung der Standard-Fehlerausgabe auf die Standardausgabe:

  print `/pfad/zum/kommando arg1 arg2 2>&1`;

13.3. Wie realisiere ich einen Dateidownload mit PHP?

Keywords: Datei | Download | Header | MIME-Typ

Antwort von Kristian Köhntopp

Grundsätzlich kann man einen Dateidownload auf zwei verschiedene Arten realisieren: Entweder man schreibt ein PHP-Script, das einen Redirect (siehe Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?) auf die zu ladende Datei generiert, oder man startet den Download durch das PHP-Script. Die Methode mit dem Redirect hat den Nachteil, dass Anwender die Ziel-URL des Redirect mitbekommen und später dann direkt und ungeschützt auf diese Datei zugreifen können.

Will man das verhindern, muss man den Download innerhalb von PHP abhandeln. Die zu ladenden Dateien liegen dann außerhalb der Document Root des Webservers (haben also keine URL) und sind nur durch PHP zugreifbar. In PHP sendet man den passenden MIME-Typ als Header und schickt dann die gewünschte Datei hinterher. Natürlich kann man vorher noch einen Downloadzähler aktualisieren oder überprüfen, ob der Anwender überhaupt für den Download autorisiert ist.

// $download sei der Bezeichner für die zu ladende Datei
// etwa: 
$download = $_GET['download'];

// Dieses Verzeichnis liegt außerhalb des Document Root und
// ist nicht per URL erreichbar.
$basedir = "/home/www/download";

// Übersetzung von Download-Bezeichner in Dateinamen.
$filelist = array(
  "file1" => "area1/datei1.zip",
  "file2" => "area1/datei2.zip",
  "file3" => "area2/datei1.zip"
);

// Einbruchsversuch abfangen.
if (!isset($filelist[$download]))
  die("Datei $download nicht vorhanden.");

// Vertrauenswürdigen Dateinamen basteln.
$filename = sprintf("%s/%s", $basedir, $filelist[$download]);

// Passenden Datentyp erzeugen.
header("Content-Type: application/octet-stream");

// Passenden Dateinamen im Download-Requester vorgeben,
// z. B. den Original-Dateinamen
$save_as_name = basename($filelist[$download]);
header("Content-Disposition: attachment; filename=\"$save_as_name\"");

// Datei ausgeben.
readfile($filename);

Dieses Script kann mit dem Parameter $download aufgerufen werden. Dieser Parameter wird dann in den Namen einer zu ladenden Datei übersetzt. Aus Sicherheitsgründen ist es nicht möglich, dem Script direkt Dateinamen zu übergeben - wir möchten vermeiden, dass jemand als Parameter download=../../../../../../../etc/passwd oder ähnliche Namen erfolgreich übergeben kann. Die Prüfung ist eine Variante des ersten Prüfschemas aus Wie unterscheide ich böse Variablen von guten?.

Antwort von Guido Haeger

Die Reaktion des UserAgent auf die oben genannten Header ist in den RFCs für HTTP und MIME nicht eindeutig definiert. Zusätzlich haben einige Browser Bugs bezüglich der korrekten Interpretation von Content-Disposition-Headern. Es wird dann häufig versucht, den UserAgent mit nicht standardisierten Header-Kreationen zum Anzeigen des "Speichern Unter"-Dialoges zu zwingen. Das mag bei einigen UserAgents rein zufällig zum gewünschten Verhalten führen, ist aber nicht empfehlenswert. Sofern nicht standardisierte Content-Type-Header verwendet werden, so sind diese mit einem x- einzuleiten (Content-Type: x-type/x-subtype).

Der Internet Explorer 5.5 ignoriert in vielen Fällen das Filename-Attribut des Content-Disposition-Headers und schlägt vor, die Datei nicht unter dem dort vorgegebenen Dateinamen, sondern unter dem Namen des PHP-Scriptes zu speichern. Für dieses Problem gibt es verschiedene Workarounds. Bei Verwendung des Apache als HTTP-Server kann man z.B. ModRewrite verwenden. In die .htaccess oder http.conf:

RewriteEngine on
RewriteRule download/([^/]{1,30})\.([a-z0-9]{2,4}) /download.php?filename=$1&extension=$2

Bei einem Request für /download/prospekt.pdf wird auf dem Server dann das Script /download.php?filename=prospekt&lextension=pdf aufgerufen. Wenn der Internet Explorer 5.5 das Filename-Attribut des Content-Disposition-Headers ignoriert, so wird er den String hinter dem letzten Slash des URL als Dateinamen für die zu speichernde Datei anbieten. In diesem Beispiel also prospekt.pdf.

13.4. Wie kann ich in einer Datei eine Zeile einfügen oder löschen?

Antwort von Kristian Köhntopp

Für dieses Problem gibt es keine elegante oder effiziente Lösung. Die Ursache liegt darin, wie Unix und Windows die unterliegenden Dateien handhaben, nämlich als unstrukturierte Byteströme. Für diese Byteströme gibt es keine Indices und auch keine Methoden, mit denen man effizient beliebige Teile der Datei löschen oder in die Datei einfügen könnte.

Tatsächlich ist der Wunsch nach einfachen Einfüge- und Löschoperationen der Auslöser für die Schaffung von Datenbankfunktionen wie die DBM-Funktionen oder von ganzen Datenbanken wie MySQL gewesen. Wenn man auf diese Sorte Problem trifft, sollte man also intensiv über den Einsatz von DBM-Dateien oder Datenbanken nachdenken.

Um in einer Datei eine Zeile einzufügen oder zu löschen, muss man die Datei öffnen und zeilenweise durchlesen und in eine zweite Datei schreiben. Erreicht man die gewünschte Position, muss man dort eine Zeile einfügen oder löschen. Nach Abschluß der Operation ist die Originaldatei zu löschen und die neue Datei umzubenennen. Dabei ist zu beachten, dass in einer Webumgebung ohne weiteres mehrere Benutzer zugleich eine solche Operation für dieselbe Datei anfordern können. Man muss also auch durch Locking dafür Sorge tragen, dass sich diese Benutzer nicht in die Quere kommen.


// Shared lock auf die Quelldatei
$old = fopen($oldfile, "r");
flock($old, 1) or die("Kann die Quelldatei $oldfile nicht locken.");

// Exclusive lock auf die Zieldatei
$new = fopen($oldfile.".new", "w");
flock($new, 2) or die("Kann die Zieldatei $newfile nicht locken.");

$lineno = 0;

while($line = fgets($old, 1024)) {
  if ($lineno++ == $zielzeile)
    continue;  // Zeile auslassen

  fputs($new, $line);
}

fclose($old); // Gibt das Lock automatisch auf

// Alte Datei wegwerfen.
unlink($oldfile);

// Neue Datei umbenennen.
// (In Windows müssen das rename() und das fclose($new)
//  vertauscht werden, da es nicht möglich ist, in Windows
//  eine offene Datei umzubenennen.
rename($oldfile.".new", $oldfile);

// Neue Datei schließen und dabei Lock aufgeben.
fclose($new);

13.5. Wie kann ich eine Datei zeilenweise rückwärts auslesen?

Keywords: Datei | Anfang | Ende | lesen | Zeile | Datenbank | rueckwaerts

Antwort von Johannes Frömter

Dateien sind nicht in "Zeilen" organisiert, die Zeilenende-Zeichen (\n und/oder \r) sind ganz gewöhnliche Bytes im Datenstrom. Für das Lesen vom Dateianfang bis zum nächsten Zeilenende-Zeichen gibt es entsprechende Funktionen ( fgets() ), in umgekehrter Richtung ist dies jedoch wegen der physikalischen Organisation des Dateisystems nicht sinnvoll möglich.

Man muss die Datei also zuerst vorwärts zeilenweise in ein Array einlesen - das erledigt die Funktion file() - und kann dann auf dieses Array beliebig, z. B. auch von hinten nach vorne, zugreifen.

Beim Schreiben müssen die "Zeilen" nicht umständlich vorne in die Datei eingefügt, sondern können einfach hinten an die Datei angehängt werden; hierzu wird die Datei im Append-(Anhängen)-Modus geöffnet:

<?php
  // In Datei schreiben (anhängen)
  $fp = fopen($filename, 'a+');
  flock($fp, 2) or die('Kann die Datei nicht locken');
  fwrite($fp, "$zeile\n");
  fclose($fp);
?>

<?php
  // Datei "zeilenweise" in ein Array einlesen
  $array = file($filename);

  // $array von vorne nach hinten durchlaufen
  foreach($array as $zeile) {
      echo trim($zeile) . "<br>\n";
  }

  // $array von hinten nach vorne durchlaufen
  $i = sizeof($array);
  while ($i--) {
      echo trim($array[$i]) . "<br>\n";
  }
?>

file() liest die Zeilen einschließlich der Zeilenende-Zeichen ein; da die letzte Zeile in der Datei nicht unbedingt ein Zeilenende-Zeichen hat, sollte trim() bzw. rtrim() angewendet werden, um einheitliche Zeilen zu bekommen.

13.6. Wie kann ich einen Datei-Upload per FTP durchführen?

Keywords: Datei | Upload | FTP | Modul | Formular | HTTP

Antwort von Johannes Frömter

PHP hat eingebaute FTP-Funktionen , mittels derer man Dateien von oder zu einem FTP-Server übertragen kann. Es ist damit aber nicht möglich, Daten von einem Browser zu empfangen! Dies ist also keine Alternative zum HTTP-Upload per HTML-Formular (siehe hierzu "Wie funktioniert ein Datei-Upload über HTML-Formulare?").

PHP kann nur einen FTP-Client darstellen, der sich zu einem FTP-Server verbindet, d. h. der Verbindungsaufbau und die -steuerung müssen immer vom PHP-Script aus erfolgen, und die Daten für den Upload müssen sich bereits auf dem Webserver befinden. Vollständige Beispiele für eine FTP-Verbindung mit PHP finden sich im Manual und weiter unten auf dieser Seite.

Antwort von Kristian Köhntopp

Die FTP-Funktionen sind verfügbar, wenn PHP mit der Option --enable-ftp (bei PHP 3: --with-ftp) übersetzt worden ist. In der Ausgabe von phpinfo() erscheint dann in der Modulliste der FTP-Support.

Die Funktionen sind im Manual im Kapitel FTP-Funktionen im Einzelnen beschrieben.

Vollständiges Beispiel:

<h1>FTP Test</h1>
<?php
  $link = ftp_connect("localhost");
  if (!ftp_login($link, "ftp", "user@host.de"))
    die("Kann mich nicht einloggen.");

  if (!ftp_chdir($link, "/pub"))
    die("Kann nicht in das Zielverzeichnis /pub wechseln.");

  $name = ftp_nlist($link, ".");
  if (isset($name) and is_array($name)) {
    foreach ($name as $k => $v) {
      printf("%s - %s<br>\n", $k, $v);
    }
  }

  $size = ftp_size($link, "beispiel");
  if ($size < 0)
    die("Kann die Größe der Datei beispiel nicht bestimmen.");

  $mtime = ftp_mdtm($link, "beispiel");
  if ($mtime < 0)
    die("Kann die mtime der Datei beispiel nicht bestimmen.");

  printf("beispiel - %d Byte, %s<br>\n",
    $size,
    strftime("%c", $mtime));

  $result = ftp_get($link, "/tmp/bbb", "beispiel", FTP_BINARY);
  if (!$result)
    die("Download von Datei beispiel fehlgeschlagen.");

  if (!ftp_chdir($link, "/incoming"))
    die("Kann nicht in das Zielverzeiczhnis /incoming wechseln.");

  $result = ftp_put($link, "upload.txt", "/etc/termcap", FTP_BINARY);
  if (!$result)
    die("Upload von Datei termcap fehlgeschlagen.");

  ftp_quit($link);

  printf("Ende.<br>\n");
?>

13.7. Unix: Welche Zugriffsrechte brauche ich, um eine Datei anzulegen?

Keywords: Datei | Recht | neu | anlegen | Unix | Benutzer

Antwort von Kristian Köhntopp

Um in Unix eine Datei anlegen zu können, benötigt ein Programm x-Rechte an jedem Verzeichnis entlang des Pfadnamens sowie w-Recht an dem Verzeichnis, in dem die Datei angelegt werden soll.

w-Recht an einem Verzeichnis berechtigt nicht nur dazu, eine Datei neu anzulegen, sondern es berechtigt außerdem dazu, die Namen existierender Dateien aus dem Verzeichnis zu entfernen. Dies wird in Unix oft vereinfachend als "das Löschen einer Datei" bezeichnet. Beim Entfernen von Namen aus einem Verzeichnis werden die Rechte an der Datei und ihr Eigentümer nicht geprüft.

Dies ändert sich, setzt man an dem Verzeichnis, in dem sich die Datei befindet, zusätzlich das t-Recht. In diesem Fall können nur der Superuser (genauer: jeder Prozeß der CAP_FOWNER Capability hat), der Eigentümer des Verzeichnisses oder der Eigentümer der Datei noch den Namen aus dem Verzeichnis entfernen.

Um eine existierende Datei zu überschreiben, ist dagegen x-Recht an jedem Verzeichnis entlang des Pfadnamens notwendig, sowie w-Recht an der zu verändernden Datei (oder man hat CAP_FOWNER Capability).

In Apache mit suexec ist es so, dass CGI-Programme mit derjenigen User-ID und Group-ID ausgeführt werden, die für den virtuellen Webserver mit Hilfe der Direktiven User und Group definiert worden sind. CGI-PHP legt also Dateien mit diesen User- und Group-Rechten an und das Zielverzeichnis muss entsprechende Rechte besitzen, damit ein fopen() mit dem Anlegen einer Datei erfolgreich sein kann.

In Apache mit mod_php ist es so, dass der PHP-Interpreter als Bestandteil des Webservers läuft und mit den Rechten des Webservers Dateien anlegt oder löscht. In diesem Fall ist ausschlaggebend, was die globale (serverweite) User- und Group-Direktive für den Webserver festlegt. Das Zielverzeichnis muss für diesen Unix-Benutzer passende Rechte anbieten.

In PHP kann es sein, dass zusätzliche Beschränkungen gelten, die in Kraft treten, falls safe_mode aktiviert ist. Dies kann in CGI-PHP in der php.ini geschehen, in mod_php zusätzlich in <Directory>-Blöcken in der zentralen httpd.conf, jedoch nicht in .htaccess-Dateien.

Es ist empfehlenswert, für jeden virtuellen Webserver eine gesonderte CGI-Identität anzulegen und jedem virtuellen Webserver ein Verzeichnis einzuräumen, das keine URL hat (nicht unterhalb der Document Root liegt) und das nur durch diese CGI-Identität beschreibbar ist. Auf diese Weise kann jeder virtuelle Webserver anwendungsspezifische Daten auf eine Weise speichern, die nicht durch das http-Protokoll erreichbar ist und die nicht durch andere CGI-Programme anderer virtueller Webserver gelesen oder geschrieben werden koennen.

Wird mod_php verwendet, ist ein solcher Schutz nur mit safe_mode erreichbar. Dies birgt jedoch andere Nachteile.

13.8. Wie kann ich mit PHP auf die serielle Schnittstelle zugreifen?

Antwort von Matthias P. Wuerfl

Da PHP serverseitig interpretiert und ausgeführt wird, ist es nicht möglich, mit PHP auf die serielle Schnittstelle eines Clients zuzugreifen. Allerdings besteht die Möglichkeit, auf die seriellen Schnittstellen des Computers zuzugreifen, auf dem das Script ausgeführt wird. Hierzu müssen jedoch die entsprechenden Rechte gesetzt sein, so dass der PHP-Interpreter oder der Webserver auch darauf zugreifen dürfen.

Windows und Unixe stellen die serielle Schnittstelle im Filesystem zur Verfügung und man kann mit den normalen Dateisystemfunktionen darauf zugreifen. Um Daten an die serielle Schnittstelle zu senden, genügt schon ein sehr kleines Script:

$string = "Hallo Schnittstelle!\n";
$pointer = fopen("/dev/ttyS0","w");
fwrite ($pointer, $string);
fclose($pointer);

In diesem Beispiel wird die serielle Schnittstelle /dev/ttyS0 zum Schreiben geöffnet, ein String wird hineingeschrieben und die Schnittstelle wird wieder geschlossen. Analog dazu könnte man die Schnittstelle auch zum Lesen öffnen und mit fgets() auf Input warten. Hierbei ist allerdings zu beachten, dass das Script nicht weiterarbeitet, bis nicht eine Zeile eingelesen ist. Können von der seriellen Schnittstelle keine Daten gelesen werden, so stoppt das Script beim fgets() und wartet bis zum Timeout.

Die serielle Schnittstelle kann nicht zum gleichzeitigen Lesen und Schreiben geöffnet werden; jedoch kann ein Script lesend darauf zugreifen, während ein anderes schreibt. Da die seriellen Schnittstellen einen bunten Strauß an Konfigurationsmöglichkeiten kennen, ist die Konfiguration der Schnittstelle auf Betriebssystemebene mitunter etwas kniffelig. Bevor man mit PHP darauf zugreift, sollte man seine Konfiguration mit den Mitteln des Betriebssystems testen, um bei der Erstellung eines PHP-Scriptes eine falsche Schnittstellenkonfiguration als Grund für das Nichtfunktionieren auszuschließen.

13.9. Warum funktioniert unlink() unter Windows nicht?

Keywords: Datei | loeschen | unlink | Windows | Fehler

Antwort von Martin Jansen

In einigen älteren Versionen von PHP 4 ist die Funktion unlink() unter Windows nicht implementiert, was sich in der Fehlermeldung

Warning: unlink() is not supported in this PHP build in ...
on line ...

ausdrückt. Abhilfe schafft hier ein Update auf eine aktuelle Version von PHP 4.

13.10. Wie wende ich include() mit verschachtelten Verzeichnissen an?

Antwort von Johannes Frömter

Mit include("/subdir/foo.php") bindet man ein Script in einem untergeordneten Verzeichnis ein. In foo.php will man nun die ebenfalls in /subdir stehende Datei bar.php einbinden. Ein einfaches include("bar.php") funktioniert nicht, da PHP die Pfadangabe nicht relativ zum Script mit der include() -Anweisung, sondern relativ zum Ursprungsscript benötigt.

Mit Hilfe der Konstanten __FILE__ kann man sich den richtigen Pfad zusammenschrauben:

include(dirname(__FILE__)."/bar.php");

13.11. Wie übergebe ich Variablen an eingebundene Dateien?

Antwort von Johannes Frömter

Gar nicht. Mittels include() oder require() in ein PHP-Programm importierte Scriptteile "erben" automatisch die Variablen, die an dieser Stelle des Scriptes vorhanden sind (siehe auch: Geltungsbereich von Variablen im Manual). Einleuchtend wird dies, wenn man sich vergegenwärtigt, dass durch include() der einzubindende Scriptteil quasi an die Stelle des Befehls kopiert wird.

// $var soll an script.php übergeben werden

// Funktioniert nicht
include('script.php?var=test');

// So geht das
$_GET['var'] = 'test'; // oder bei altem script.php $var = 'test';
include('script.php');

// So geht's bei externen Dateien
include('http://example.com/script.php?var=test');

Versucht man bei lokalen Dateien eine Variablenübergabe wie bei HTTP (erstes Beispiel), meldet PHP, dass es die Datei script.php?var=test nicht finden kann - was auch völlig korrekt ist, da es eine Datei mit diesem Namen nicht gibt ...

Antwort von Kerry W. Lothrop

Beim Übergeben der Parameter durch vorheriges Setzen (also beim Aufruf über das Dateisystem) ist die Methode stark davon abhängig, wie in der eingebundenen Datei auf die externe Variablen zugegriffen wird. Die Variablen sollten dann immer so gesetzt werden, wie sie auch im eingebundenen Skript angesprochen werden, also z.B. $HTTP_GET_VARS['var'] oder $_POST['var'].

Beim Einbinden externer Dateien, die die Parameter über die Arrays $_POST oder $HTTP_POST_VARS ansprechen, muss ein POST-Request formuliert werden (siehe Wie kann ich einen HTTP POST-Request absenden?).

14. Datums- und Kalenderprobleme

14.1. Wie kann ich das aktuelle Datum bekommen?

Keywords: time | strftime | date

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion time() bekommt man den Unix-Timestamp, der sich mit anderen Funktionen ( strftime() ) weiterverarbeiten lässt. Mit der Funktion date() kann man das Datum direkt als String formatiert abrufen.

14.2. Wie kann ich ein deutsches Datum in MySQL-Format umwandeln (und umgekehrt)?

Antwort von Kristian Köhntopp

MySQL verarbeitet Datumsangaben im ISO-8601-Format (siehe die Abhandlung von Markus Kuhn zu diesem Thema). Dies ist das offizielle deutsche Datumsformat, eine Umwandlung ist nicht notwendig, weil nicht normgerecht.

Dennoch kann man das Datum auch in anderen Formaten bekommen. Wahlweise kann die Umwandlung bereits in MySQL oder erst in PHP geschehen. In MySQL kann man mit Hilfe der Funktion date_format das Datum in beliebigen Formaten bekommen:

mysql> SELECT date_format(changed, '%d.%m.%Y %H:%i:%s') AS datum
    -> FROM teiln_liste;
+---------------------+
| datum               |
+---------------------+
| 27.08.1998 12:49:29 |
| 14.12.1999 15:05:52 |
| 13.12.1999 08:30:43 |
| 13.05.1998 15:51:45 |
| 06.10.1998 14:30:25 |
| 07.08.1998 11:28:59 |
| 23.06.1998 17:15:16 |
| 14.01.1999 08:34:22 |
| 07.01.2000 11:36:42 |
| 01.02.1999 08:47:25 |
+---------------------+
10 rows in set (0.00 sec)

In PHP kann man mit Hilfe der Funktion date() aus einem time_t ein beliebiges Datum generieren und mit Hilfe der Funktion mktime() aus den Fragmenten einer Datumsangabe einen time_t erzeugen. Mit den time_t (Sekunden seit Mitternacht GMT, 1. Januar 1970) lassen sich sehr natürlich Zeitdifferenzen bestimmen und andere Zeitrechnungen ausführen.

Will man dagegen nur einen MySQL TIMESTAMP oder DATE in andere Reihenfolge umsortieren, kann man stattdessen mit den Funktionen explode() oder substr() arbeiten.

/**
 * date_mysql2german
 * wandelt ein MySQL-DATE (ISO-Date)
 * in ein traditionelles deutsches Datum um.
 */
function date_mysql2german($datum) {
    list($jahr, $monat, $tag) = explode("-", $datum);

    return sprintf("%02d.%02d.%04d", $tag, $monat, $jahr);
}

/**
 * date_german2mysql
 * wandelt ein traditionelles deutsches Datum
 * nach MySQL (ISO-Date).
 */
function date_german2mysql($datum) {
    list($tag, $monat, $jahr) = explode(".", $datum);

    return sprintf("%04d-%02d-%02d", $jahr, $monat, $tag);
}

/**
 * timestamp_mysql2german
 * wandelt ein MySQL-Timestamp
 * in ein traditionelles deutsches Datum um.
*/
function timestamp_mysql2german($t) {
    return sprintf("%02d.%02d.%04d",
                    substr($t, 6, 2),
                    substr($t, 4, 2),
                    substr($t, 0, 4));
}

14.3. Wie kann ich die Anzahl der Tage zwischen zwei Daten bestimmen?

Antwort von Kristian Köhntopp

Dazu gibt es verschiedene Lösungsansätze. Beispielsweise kann man beide Daten in Julianische Tage verwandeln und sie dann voneinander subtrahieren. Das geschieht mit der Funktion GregorianToJD() aus der optionalen Kalenderbibliothek von PHP (muss erst kompiliert werden).

Antwort von Alex Kiesel

Eine zweite Möglichkeit ist, die Datumsangaben in Unix-Timestamps umzuwandeln, mit denen die Differenz durch einfache Substraktion ermittelt werden kann. Unix Timestamps geben eine Zeit als die Anzahl der vergangenen Sekunden seit dem Beginn der Unix Epoche an. Folgender Code demonstriert das:

$utimeDatum1= mktime(0, 0, 0, 1, 1, 2000);  // 1. Januar 2000
$utimeDatum2= time ();                      // Jetzt

$diff= $utimeDatum2 - $utimeDatum1;
printf ("Seit dem %s sind %d Tage bis heute vergangen\n",
  date ('d.m.Y', $utimeDatum1),
  ($diff / 86400)
);
  

Der Nachteil am Rechnen mit Unix-Timestamps besteht darin, dass man nicht mit Daten vor dem 13.12.1901, 20:45:54 GMT und Daten nach dem 19.01.2038 03:14:07 GMT rechnen kann; zudem unterstützen manche verschiedenen Linux- und Windowssystemen Probleme keine Daten im negativen (Integer-)Bereich, also vor dem 1.1.1970.

14.4. Wie kann ich das Datum des Vortages bestimmen?

Keywords: Gestern | date | mktime

Antwort von Kristian Köhntopp

Wenn man date() und mktime() kombiniert, kann man Datumsberechnungen durchführen. Die Funktion mktime() berechnet automatisch den korrekten Wert für Überläufe - der 32.12.1997 wird richtig in 01.01.1998 umgewandelt. So erhält man also auch das Datum des Vortages:

$tstamp  = mktime(0, 0, 0, date("m"), date("d")-1, date("Y"));
$gestern = date("Y-m-d", $tstamp);  // ISO-8601 Format
print $gestern;

Beachte: Die Reihenfolge der Parameter in der PHP-Funktion mktime() entspricht nicht der Reihenfolge der Parameter in der C-Funktion gleichen Namens.

14.5. Wieviel Tage hat der aktuelle Monat?

Keywords: Monat | Monatslaenge | Februar

Antwort von Kristian Köhntopp

date() versteht die Option "t", die die Anzahl der Tage im Monat zurückliefert:

$tage = date("t");

14.6. Wie kann ich die Datumsausgabe auf Deutsch umstellen?

Antwort von Johannes Frömter

Mit der Funktion setlocale() kann man diverse Länder- und sprachabhängigen Einstellungen vornehmen. Die Sprache wird im Format Sprache_Land bzw. Sprache_Land.Codepage angegeben und ist plattformabhängig; unter Unix/Linux sind zweibuchstabige Länderabkürzungen zu verwenden, also z.B. de_DE für Deutsch/Deutschland oder de_AT für Deutsch/Österreich, Windows dagegen erwartet German_Germany bzw. German_Austria. Die Einstellung für die Kategorie LC_TIME wirkt sich nur auf die Ausgabe von strftime() , nicht jedoch auf date() aus.

<?php
  // Default
  echo strftime('%A, %d. %B %Y', 1009926000);
  // Ausgabe: Wednesday, 02. January 2002

  Linux:   setlocale(LC_TIME, 'de_DE');
  *BSD:    setlocale(LC_TIME, 'de_DE.ISO_8859-1');
  Win:     setlocale(LC_TIME, 'German_Germany');
  echo strftime('%A, %d. %B %Y', 1009926000);
  // Ausgabe: Mittwoch, 02. Januar 2002

  Linux:   setlocale(LC_TIME, 'de_AT');
  *BSD:    setlocale(LC_TIME, 'de_AT.ISO_8859-1');
  Win:     setlocale(LC_TIME, 'German_Austria');
  echo strftime('%A, %d. %B %Y', 1009926000);
  // Ausgabe: Mittwoch, 02. Jänner 2002
?>

Um zu testen, ob ein bestimmter locale-String auf einem System unterstützt wird, kann man den Rückgabewert von setlocale() auswerten:

if (setlocale(...) === false)  // dann hat's nicht funktioniert

Warnung: setlocale() ist nicht thread-safe, d.h. die Einstellungen wirken sich in Multithread-Umgebungen u.U. auch auf andere, parallel laufende Scripte aus!

15. Mail lesen und schreiben

15.1. Was ist SMTP?

Keywords: Mail | SMTP | Protokoll | Grundlagen

Antwort von Kristian Köhntopp

SMTP ist das Simple Mail Transport Protocol, das Protokoll, das im Internet verwendet wird, um Mail bei einem Mailserver einzuliefern und Mail zwischen zwei Mailservern auszutauschen. SMTP ist ein textorientiertes Protokoll, das auf dem TCP-Port 25 abgewickelt wird. Daher kann man es mit dem Kommando telnet leicht simulieren und debuggen.

kris@valiant:~ > telnet mail 25
Trying 193.102.57.5...
Connected to white.koehntopp.de.
Escape character is '^]'.
220 white.koehntopp.de ESMTP Sendmail 8.9.3/8.9.3/SuSE Linux 8.9.3-0.1
HELO valiant.koehntopp.de
250 white.koehntopp.de Hello valiant.koehntopp.de [193.102.57.3], [..]
MAIL FROM: kris@koehntopp.de
250 kris@koehntopp.de... Sender ok
RCPT TO: kris@koehntopp.de
250 kris@koehntopp.de... Recipient ok
DATA
354 Enter mail, end with "." on a line by itself
From: ab@sen.der
To: Mailingliste der Empfaenger <in@vali.de>
Subject: Testmail

Text der Nachricht
.
250 LAA08243 Message accepted for delivery
QUIT
221 white.koehntopp.de closing connection
Connection closed by foreign host.

Nachdem die Verbindung mit dem empfangenden Mailer hergestellt worden ist, identifiziert sich der absendende Rechner mit dem Kommando HELO. Falls dieser Rechner berechtigt ist, überhaupt Mail einzuliefern, antwortet der empfangende Rechner mit einem Statuscode (hier: 250) und einer Textmeldung.

Der Absender nennt jetzt den tatsächlichen Absender (MAIL FROM) und den tatsächlichen Empfänger (RCPT TO, "Receipt to") der Mail. Nach dem Kommando DATA beginnt die Übertragung der eigentlichen Mailnachricht, die mit einem einfachen Punkt abgeschlossen wird (das Protokoll verlangt, dass Punkte am Anfang einer Zeile, die Teil der Nachricht sind, verdoppelt werden).

Die Absender und Empfänger im Text der Mail sind bedeutungslos und werden vom Mailer nicht weiter ausgewertet: Entscheidend sind sie nur ganz am Anfang, wenn die Mail auf den Weg gebracht wird, ähnlich wie bei einem Brief, der in einen Umschlag getütet wird. Von Bedeutung sind nur die Absender und Empfänger im MAIL FROM und RCPT TO-Dialog. Diese beiden Adressen nennt man Envelope-From und Envelope-To, nach den Anschriften, die außen auf einem Briefumschlag (Envelope) stehen und für die Zustellung wichtig sind.

Dies bewirkt zum Beispiel, dass die Empfänger einer Mailingliste nicht alle in der To:-Zeile des Headers aufgelistet sind: Im To: steht nur der Name der Liste, der Listenroboter (Mailexploder) expandiert diesen Namen jedoch zu einer Liste der Empfänger im Envelope. Der Envelope-From einer Nachricht ist nach der Auslieferung einer Mail in eine Mailbox als From-Header (From-Space im Gegensatz zu From-Doppelpunkt, dem Body-From) ganz am Anfang der Nachricht enthalten. Der Envelope-To ist verschwunden, weil er nicht mehr gebraucht wird - daher ist es nicht möglich, eine einzelne POP3-Mailbox nach dem Download mit Fetchmail oder anderen Werkzeugen korrekt im Hause weiter aufzuteilen: Der tatsächliche Empfänger steht nicht mehr zur Verfügung und die Body-To-Zeilen liefern nur Anhaltspunkte.

15.2. Was ist das Domain Name System?

Keywords: Mail | DNS | Domain | Name | IP | MX | Server | nslookup | Record | sendmail

Antwort von Kristian Köhntopp

Das Domain Name System ist eine verteilte Datenbank, die Namen in Domainform (ein.wort.mit.punkten.darin) beliebige getypte textuelle Information zuordnen kann. Man kann dem Domain Name System Fragen stellen, die die Form (name, typ) haben und bekommt dann einen oder mehrere Ressource Records des passenden Typs als Antwort.

Wichtige Ressource-Records sind A- und AAAA-Records, die die IP-Nummern von Rechnern darstellen, A-Records für IPv4-Adressen und AAAA-Records für IPv6-Adressen. Außerdem sind für die Mailzustellung MX-Records besonders wichtig, weil sie den für den Mailempfang zuständigen Rechner bezeichnen. Ebenfalls häufig findet man PTR-Records (Umwandlung von IP-Nummern zurück in Namen) und NS-Records (Finden von zuständigen Nameservern).

Mit Hilfe des Werkzeuges nslookup kann man in Unix und Windows Anfragen an das DNS stellen, außerdem kann man bequemer das Unix-Programm dig verwenden.

Ein Mailer wie das Unix-Programm sendmail richtet sich bei der Mailzustellung nach den MX-Records (MX steht für mail exchanger) einer Domain.

kris@valiant:~ > nslookup
Default Server:  nuki.netuse.de
Address:  193.98.110.1

> set type=mx
> php.net.
Server:  nuki.netuse.de
Address:  193.98.110.1

Non-authoritative answer:
php.net mail exchanger = 10 synacor1.php.net.

Authoritative answers can be found from:
php.net nameserver = ns2.easydns.com.
php.net nameserver = remote1.easydns.com.
php.net nameserver = remote2.easydns.com.
php.net nameserver = ns1.easydns.com.
synacor1.php.net        internet address = 208.210.50.162
ns1.easydns.com internet address = 216.220.40.243
ns2.easydns.com internet address = 216.220.40.244
remote1.easydns.com     internet address = 64.39.29.212
> ^D
kris@valiant:~ >

kris@valiant:~ > dig php.net mx

; <<>> DiG 9.1.3 <<>> php.net mx
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 864
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 4

;; QUESTION SECTION:
;php.net.                       IN      MX

;; ANSWER SECTION:
php.net.                21358   IN      MX      10 synacor1.php.net.

;; AUTHORITY SECTION:
php.net.                21358   IN      NS      ns2.easydns.com.
php.net.                21358   IN      NS      remote1.easydns.com.
php.net.                21358   IN      NS      remote2.easydns.com.
php.net.                21358   IN      NS      ns1.easydns.com.

;; ADDITIONAL SECTION:
synacor1.php.net.       21358   IN      A       208.210.50.162
ns1.easydns.com.        140984  IN      A       216.220.40.243
ns2.easydns.com.        140984  IN      A       216.220.40.244
remote1.easydns.com.    140984  IN      A       64.39.29.212

;; Query time: 23 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Apr 24 19:09:47 2002
;; MSG SIZE  rcvd: 205

Im Beispiel mit nslookup, das so unter Unix und unter Windows NT funktioniert, kann man sehen, dass der lokale Domain Name Server als Datenquelle verwendet wird. Durch das Kommando set type=mx legen wir fest, dass wir Informationen über MX-Records haben möchten. Durch einfaches Eingeben eines Namens (hier: php.net) wird die Anfrage abgesendet und wir erhalten zwei Sorten von Informationen:

  • Der lokale Server hat uns die nicht-abschließende Information geliefert, dass Mail an php.net an den Rechner synacor1.php.net ausgeliefert werden soll. Wenn wir also eine Mailzustellung an eine Adresse der Form bla@php.net vornehmen sollen, müssen wir uns mit synacor1.php.net, Port 25 verbinden und die Mail dort abliefern.

  • Diese Information ist jedoch nicht-abschließend, weil der lokale Nameserver nicht Eigentümer der Domain php.net ist. Wenn wir abschließende Information wünschen, müßten wir uns an einen der vier definitiven Nameserver für die Domain wenden, die uns genannt werden: ns1.easydns.com, ns2.easydns.com, remote1.easydns.com oder remote2.easydns.com und die Frage dort noch einmal stellen. Die Adressen dieser Server werden uns auch gleich mitgeliefert.

Die Ausgabe von dig liefert diese Information noch einmal, nur in besser lesbarer und übersichtlicherer Form.

15.3. Unix: Wie funktioniert der Mailversand?

Keywords: Mail | Unix | Versand | sendmail | Mailserver

Antwort von Kristian Köhntopp

Ein Unix-System hat in der Regel ein Mailprogramm installiert, das Anfragen an das DNS-System stellen kann oder auf andere Weise die zur Auslieferung der Mail benötigten Informationen beschaffen kann. In den meisten Fällen ist dieses Mailprogramm /usr/lib/sendmail (oder /usr/sbin/sendmail) und versteht eine Option -t, die bewirkt, dass man das Programm starten und eine Mail mit allen benötigten Headern auf der Standardeingabe hinterlassen kann. Das Programm wird dann auf der Basis der erhaltenen Headerinformationen den Envelope generieren, den Header vervollständigen und ggf. korrigieren, bevor die Mail dann abgesendet wird.

Der einfachste Weg, um ein Programm zu starten und dann seine Standardeingabe mit Daten zu füllen, ist die C-Funktion popen(). Die PHP-Funktion mail() verwendet diese C-Funktion, um das durch die Konfigurationsvariable sendmail_path definierte Mailprogramm zu starten und mit den benötigten Daten zu füttern.

  mail("em@pfaeng.er",
     "Testmail",
     "Dies ist nur eine Testnachricht.",
     "From: ab@send.er\r\nReply-To: devnull@send.er");

Hinweis: In vielen Versionen von PHP unter Windows funktioniert die Variante von mail() mit drei Parametern wegen eines Fehlers im Interpreter nicht. In diesem Fall ist die Funktion mail() zwingend mit vier Parametern aufzurufen, etwa indem als vierter Parameter der Wert Content-Type: text/plain; charset=iso-8859-1 angegeben wird.

Alternativ kann man diese Funktionalität auch manuell in PHP nachprogrammieren, also die PHP-Funktion popen() aufrufen und die Mail dann ausgeben. Beide Ansätze sind funktional gleich (aber mail() ist weniger Arbeit).

  $fd = popen("/usr/sbin/sendmail -t ","w");
  fputs($fd, "To: em@pfaeng.er\r\n");
  fputs($fd, "From: ab@send.er\r\n");
  fputs($fd, "Reply-To: devnull@send.er\r\n");
  fputs($fd, "Subject: Testmail\r\n\r\n");
  fputs($fd, "Das ist nur eine Testnachricht.\r\n");
  pclose($fd);

15.4. Windows: Wie funktioniert der Mailversand?

Keywords: Windows | Mail | Versand | Mailserver | SMTP

Antwort von Kristian Köhntopp

In Windows kann man nicht davon ausgehen, dass wie in Unix ein Mailer installiert ist, der Mail selber zustellen kann. Daher muss man dem Windows-System einen Rechner mitteilen, der einen Mailer installiert hat, den das Windows-System über TCP/IP mitbenutzen darf. Das kann der lokale Rechner localhost sein, falls auf dem eigenen System ein Mailserver installiert ist, aber auch jedes andere System, das für uns seinen Relay- und Spamschutz abgeschaltet hat.

Die Funktion mail() baut unter Windows eine TCP/IP-Verbindung zum Port 25 dieses in der Konfigurationsvariable SMTP festgelegten Systems auf und erzeugt mit dem oben gezeigten SMTP-Dialog eine Mail. Dabei wird die in der Konfigurationsvariablen sendmail_from festgelegte Absenderadresse verwendet.

Anders als in Unix hat man hier also nicht die Freiheit, den Absender oder Blindkopienempfänger in der Mailfunktion frei definieren zu können, weil der entfernte Mailer diese Daten von uns nicht annimmt oder bei der späteren Headerkorrektur wieder überschreibt.

Alternativ kann man die Arbeit der Mailfunktion auch manuell nachprogrammieren - dabei sollte man jedoch bedenken, dass ein SMTP-Server nicht jede Headerzeile so annimmt oder unverfälscht durchläßt, wie man sie ihm vorwirft! Der hier gezeigte Code ist sehr unzuverlässig, denn er hat keine richtige Fehlerprüfung.

$hdr  = "From: ab@send.er\r\n";
$hdr .= "To: em@pfaeng.er\r\n";
$hdr .= "Reply-To: devnull@send.er\r\n";
$hdr .= "Subject: Testmail\r\n";
$hdr .= "\r\n";

# Socket oeffnen.
$fp = fsockopen("mail.server.de", 25);
$result = fgets($fp, 1024);
if ($result+0 != 220)
	die("Statuscode falsch (service not ready?): $result");

# HELO
fputs($fp, "HELO mein.eigener.servername.de\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
	die("HELO Statuscode falsch: $result");

# MAIL FROM
fputs($fp, "MAIL FROM: ab@send.er\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
	die("MAIL FROM Statuscode falsch: $result");

# RCPT TO
fputs($fp, "RCPT TO: em@pfaeng.er\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
	die("RCPT TO: Statuscode falsch: $result");

# DATA
fputs($fp, "DATA\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 354)
	die("DATA: Statuscode falsch: $result");

# Header senden
fputs($fp, $hdr);

# Text senden
fputs($fp, "Das ist nur eine Testnachricht.");

# Ende von DATA: CRLF . CRLF
fputs($fp, "\r\n.\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
	die("DATA(end): Statuscode falsch: $result");

# QUIT
fputs($fp, "QUIT\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 221)
	die("QUIT: Statuscode falsch: $result");

# Verbindung schließen
fclose($fp);

Wer sich diesen Code nicht zutraut, kann auch die SMTP Klasse von Manuel Lemos verwenden. Sie verbirgt das manuelle Senden der Daten über den Socket gegenüber dem Programmierer und erzeugt auch Fehlermeldungen, wenn mal etwas nicht klappte. Beispiel:

<?php

  /* Funktionsklasse inkludieren. */
  include "smtp.php";

  $smtp = new smtp_class;
  $smtp->host_name=getenv("HOSTNAME");
  $smtp->localhost="localhost";
  $from = "name@".$smtp->host_name;
  $to = "da@irgendwo.de";

  $inhalt = "Inhalt der Mail";

  if ($smtp->SendMessage(
      $from,
      array($to),
      array("From: $from","To: $to","Subject: test"),
      $inhalt))
  {
     print "Mail wurde erfolgreich versandt.";
  }

?>

15.5. Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?

Antwort von Kristian Köhntopp

Wenn man in Windows keinen eigenen Mailserver hat, den man zum Mailversand nutzen kann, muss man sich einen installieren. Die folgenden Produkte wurden in der Newsgroups zu diesem Zweck genannt:

Im Prinzip kann man auch den Mailserver des Providers benutzen; hierzu ist in der php.ini im Abschnitt [mail function] bei SMTP der SMTP-Server des Providers und bei sendmail_from die eigene EMail-Adresse bei diesem Provider anzugeben. Allerdings kann es hier wegen SMTP-after-POP oder gesichertem SMTP (SMTP-AUTH-Login) Probleme geben.

15.6. Wie kann ich eine HTML-Mail versenden?

Keywords: mail | html | MIME

Antwort von Kristian Köhntopp

Die Mailfunktion in PHP hat einen optionalen vierten Parameter, mit dem man zusätzliche Headerzeilen definieren kann. Diese Headerzeilen können den MIME-Type einer Mail bestimmen, den Absender der Mail festlegen oder auch beliebige Nicht-Standard-Header (X-Mailer: etc.) enthalten.

$message = "<h1>Hello world!<h1>";
$to      = "empfaenger@system.de";
$subject = "Betrefftext";
$xtra    = "From: ab@sender.de (Ab Sender)\r\n";
$xtra   .= "Content-Type: text/html\r\nContent-Transfer-Encoding: 8bit\r\n";
$xtra   .= "X-Mailer: PHP ". phpversion();

mail($to,
     $subject,
     $message,
     $xtra);

15.7. Wie kann ich ein Attachment mit einer Mail versenden?

Keywords: Mail | Attachment | Datei | versenden

Antwort von Johannes Frömter

Bei der mail() -Funktion von PHP kann man im vierten Argument jeden beliebigen zusätzlichen Header angeben. Attachments werden MIME-kodiert. Eine HTML Mime Mail class kapselt diese Funktionalität und macht die ganze Geschichte recht einfach. Unter der genannten URL finden sich auch Anwendungsbeispiele.

15.8. Wie kann ich eine Mail effizient an sehr viele Empfänger versenden?

Antwort von Kristian Köhntopp

Am günstigsten und sichersten versendet man Mail an viele Empfänger, indem man eine spezialisierte Software dafür verwendet. Empfehlenswert sind Mailinglisten-Server wie majordomo, ezmlm oder Ecartis.

Alternativ kann man sich mit einer deutlich primitiveren Lösung in PHP behelfen, indem man gemäß den Beispielen oben zusätzliche Headerzeilen mit Bcc-Empfängern erzeugt. Auf diese Weise generiert man eine einzelne Mail an viele Empfänger, die vom Mailer sehr effizient verteilt werden kann. Gleichzeitig vermeidet man durch die Verwendung von blind carbon copy (BCC)-Empfängern, dass die Empfänger im Kopf der Mail mit aufgeführt werden und auf diese Weise ein Monsterheader entsteht.

  # Empfaengerliste
  $empfaenger = array("a@example.com", "b@example.com");

  # Bcc generieren
  foreach ($empfaenger as $k => $v) {
    $bcc .= "Bcc: $v\r\n";
  }

  mail("em@pfaeng.er",
     "Testmail",
     "Dies ist nur eine Testnachricht.",
     $bcc);

15.9. Wie kann ich die Gültigkeit einer Mailadresse testen?

Antwort von Kristian Köhntopp

Der Mailer eines Systems kann die Mail dann zustellen, wenn das Domain Name System (DNS) für die Zieladresse einen Mail Exchanger (MX) Ressource Record (RR) oder einen Address (A) Ressource Record enthält. Wenn man testen möchte, ob die Empfängeradresse für eine Mail gültig ist, braucht man Zugriff auf das Internet und einen DNS-Server, den man befragen kann. Dann kann man die Anfrage, die der Mailer später einmal stellen wird, um die Mail zuzustellen, manuell mit Hilfe der Funktion checkdnsrr() nachvollziehen. Die Funktion liefert true, wenn ein passender RR vorhanden ist.

Eine DNS-Anfrage kann je nach Verfügbarkeit des DNS-Systems bis zu mehreren Minuten dauern. Der betreffende Webserverprozeß ist in diesem Zeitraum blockiert. Das Vorhandensein der benötigten RRs garantiert natürlich nicht, dass das Zielsystem auch mit uns redet oder dass der gewünschte Benutzer existiert und Mail empfangen kann. Die einzige Methode, zuverlässig zu testen, ob eine Mail zustellbar ist, ist sie zuzustellen.

  $addr = "user@host.doma.in";
  list($user, $host) = explode("@", $addr);
  if (checkdnsrr($host, "MX") or checkdnsrr($host, "A")) {
    print "Mail ist vielleicht zustellbar.<BR>\n";
  } else {
    print "Mail ist sicher nicht zustellbar.<BR>\n";
  }

Antwort von Guido Haeger

Im SIMPLE MAIL TRANSFER PROTOCOL (SMTP - RFC 0821 ) ist das Kommando VERIFY (VRFY) definiert. Hat man die Mailadresse "demouser@domain.tld" dann kann man theoretisch mittels "VERIFY demouser" beim für "domain.tld" zuständigen SMTP-Server anfragen, ob ein Postfach für "demouser" existiert. In der Praxis ist dieses Kommando aber bei fast allen SMTP-Servern deaktiviert, bzw. überhaupt nicht implementiert (Spam-/Datenschutz), so dass VERIFY keine praktikable Lösung ist.

15.10. Wie kann ich überprüfen, ob eine versendete Mail tatsächlich angekommen ist?

Antwort von Kristian Köhntopp

Manche Mailer unterstützen Delivery Status Notification (DSN) nach RFC 1894. Dies ist ein RFC auf der Internet Standards Track im Status proposed standard, er wird also in veränderter Form einmal Draft Standard und dann Internet Standard werden. Relevant im selben Zusammenhang ist die ganze Reihe der RFCs in diesem Bereich:

  • RFC 1891 (SMTP Service Extension for Delivery Status Notifications)

  • RFC 1892 (The Multipart/Report Content Type for the Reporting of Mail System Administrative Messages)

  • RFC 1893 (Enhanced Mail System Status Codes)

  • und der bereits genannte RFC 1894 (An Extensible Message Format for Delivery Status Notifications)

Die Anforderung von DSNs erfolgt mit Hilfe der in RFC 1891 beschriebenen SMTP Service Extension, ist also Bestandteil des SMTP Dialoges.

Auf Unix-Systemen wird der SMTP-Dialog durch das lokal installierte sendmail-Programm abgewickelt. Dieses versteht bestimmte Optionen (-N und -R, die in der Manualpage von sendmail beschrieben sind), mit deren Hilfe DSNs angefordert werden können. Die Option -N <reportklasse> legt fest, für welche Fälle DSNs erzeugt werden sollen. Es können mehrere Reportklassen durch Komma getrennt spezifiziert werden: failure, wenn eine Benachrichtigung bei Zustellproblemen erzeugt werden soll, delay für eine Benachrichtigung bei Zustellverzögerungen und success, für Nachricht, wenn die Zustellung erfolgreich war. Mit der Zustellbenachrichtigung wird zugleich ein Teil der Originalnachricht zurück übermittelt, damit der Absender feststellen kann, auf welche Nachricht sich die Zustellbenachrichtigung bezieht. Die Option -R <return> legt fest, wieviel von der Originalnachricht zurück übermittelt werden soll: hdrs übermittelt nur die Headerzeilen der Originalnachricht zurück, während full die komplette Originalnachricht in der Zustellbenachrichtigung zurückgibt.

Auf Unix-Systemen kann man also DSNs anfordern, wenn man einen ausreichend neuen Sendmail (8.8.x oder besser) installiert hat und man die Konfigurationsvariable sendmail_path passend setzt:

sendmail_path = /usr/lib/sendmail -N failure,success -R hdrs -t

Auf Windows-Systemen wickelt PHP den SMTP-Dialog mit einem entfernten SMTP-Mailer ab. Es sind keine Eingriffsmöglichkeiten vorgesehen, mit deren Hilfe man den SMTP-Dialog erweitern kann. Entscheidet man sich, den SMTP-Dialog manuell abzuwickeln, damit man die SMTP-Erweiterungen gemäß RFC 1891 implementieren kann, ist unbedingt darauf zu achten, dass man das Vorhandensein dieser Erweiterungen im EHLO-Kommando des SMTP-Dialoges richtig abfragt, sonst schlägt die Mailzustellung fehl, wenn man auf einem Mailer einliefert, der kein DSN kann.

Das Resultat von aktivierten DSNs sind Nachrichten mit dem Mime-Typ multipart/report wie in RFC 1892 und RFC 1894 spezifiziert. Diese Nachrichten enthalten mindestens zwei, meist jedoch drei Teile, von denen einer message/delivery-status ist und eine maschinenlesbare DSN nach RFC 1894 enthält. Mit Hilfe eines POP3 oder IMAP-Clients kann man diese Nachrichten einsammeln und analysieren.

15.11. Wie kann ich feststellen, ob eine Mailadresse äußerlich gültig ist?

Antwort von Kerry W. Lothrop

Im PEAR-Repository gibt es die Klasse Mail_RFC822, die überprüfen kann, ob eine Adresse RFC-822-konform ist.

Ein einfacher regulärer Ausdruck reicht zur Prüfung nicht aus, da RFC822-Mailadressen eine relativ komplexe Syntax haben können. Die meisten Prüfungen auf der Basis einer einfachen Regexp lehnen viele legale Adressen ab (Es existieren zum Beispiel inzwischen gültige Toplevel-Domains mit mehr als 3 Buchstaben) und lassen zugleich ungültige Adressen zu.

15.12. Wie versende ich SMS mit PHP?

Keywords: Mail | SMS | Versand | Gateway | Handy

Antwort von Martin Jansen

Generell muss man bei der Verwendung von SMS zwei Arten unterscheiden:

Auf der einen Seite gibt es die Möglichkeit, eine SMS an eine vorher definierte Rufnummer (z.B. die des eigenen Handys) zu senden.

Die zweite Möglichkeit, die sehr häufig von Portalseiten (z.B. Lycos) genutzt wird, ist, dem Besucher der Seite die Möglichkeit zu geben, an eine Rufnummer seiner Wahl eine SMS zu senden. Diese SMS enthalten allerdings in der Regel einen kurzen Werbetext.

Variante 1 lässt sich mit der mail() -Funktion von PHP realisieren: Jeder Netzanbieter stellt jedem seiner Kunden auf Nachfrage eine eigene E-Mail-Adresse (z.B. <rufnummer>@smsmail.eplus.de) zur Verfügung. Nachrichten an diese E-Mail-Adresse werden als SMS an den Mobilfunkaccount des Kunden weitergeleitet. Dieser Service kostet je nach Anbieter einen gewissenen Betrag pro SMS. So würde zum Beispiel folgendes Skript eine SMS an die Nummer 0177-1234567 senden:

<?php
$subject = "Hello World";
$text = "Just another PHP hacker";
mail("491771234567@smsmail.eplus.de",$subject,$text,"From: sms-service@send.er");
?>

Mit dieser Variante lässt sich auf einfache Weise der Versand von SMS an eine vorher definierte Nummer (i.d.R. die eigene) realisieren. Ein Risiko, das man nicht unterschätzen sollte, ist die Gefahr des Missbrauchs: Ein böswilliger User sendet zum Beispiel 100 SMS in Folge und verursacht somit beim Empfänger auf einen Schlag beträchtliche Kosten. Hier müssen entsprechende Sicherheitsmechanismen wie zum Beispiel ein Limit an SMS pro IP oder Cookies ansetzen, die aber alle keine 100%-ige Sicherheit garantieren.

Die zweite Möglichkeit verwendet ein vollwertiges SMS-Gateway, um den Versand von SMS an beliebige Nummern zu realiseren.

Das SMS-Gateway ist entweder lokal auf dem eigenen Server installiert oder man verwendet einen Fremdanbieter, der eine SMS-Gateway zu entsprechenden Konditionen zur Verfügung stellt.

Nachrichten, die über die SMS-Gateway versendet werden sollen, werden nach dem gleichen Prinzip wie Variante 1 mit der PHP-Funktion mail() versendet. Der Unterschied besteht darin, dass neben dem Text der SMS auch entsprechende Steuersignale im E-Mail-Text enthalten sind, die für die SMS-Gateway die entsprechenden Informationen liefern, wohin die SMS gesendet werden soll. Bei der Verwendung eines Fremdanbieters sind des weiteren die nötigen Authentifizierungsdaten in der E-Mail enthalten, die den SMS-Versand ermöglichen.

Selbstverständlich birgt auch diese Variante die Möglichkeit des Missbrauchs, weshalb auch hier die gleichen Sicherheitsmechanismen wie bei Variante 1 angewendet werden müssen. Die meisten der grossen Anbieter von kostenlosem SMS-Versand reduzieren so zum Beispiel die Anzahl Nachrichten, die pro IP gesendet werden dürfen, auf eine gewisse Anzahl pro Tag.

15.13. Wie kann ich den Absender meiner Mail festlegen?

Antwort von Johannes Frömter

Um im Mailprogramm des Empfängers einen bestimmten Absender anzuzeigen, muss in der Mail der From:-Header entsprechend gesetzt sein. Die Funktion mail() nimmt im vierten Parameter solche Header-Angaben entgegen:

mail("empfaenger@example.org",
     "Betrefftext",
     "Hello world!",
     "From: Absender <absender@example.com>");

Eine Mailadresse mit Namen gibt man am besten in der Form Name <Adresse> an. Weitere Headerzeilen sind durch \r\n zu trennen.

Durch diesen Header wird nur der Absender in der Mail, nicht jedoch der Envelope-From: - quasi die Absenderangabe auf dem "Briefumschlag" der Mail - festgelegt. Nicht zustellbare Mails gehen daher an den Serveradministrator, und nicht an den tatsächlichen Absender zurück.

Seit PHP Version 4.0.5 kennt mail() hierzu einen fünften Parameter, dessen Inhalt direkt an das Mailprogramm weitergereicht wird. Im Falle des üblichen sendmail übergibt man den Absender für das Envelope-From: folgendermaßen:

mail("empfaenger@example.org",
     "Betrefftext",
     "Hello world!",
     "From: Absender <absender@example.com>",
     "-f absender@example.com");

Normalerweise fügt sendmail dann auch ein X-Authentication-Warning in den Header ein; um diese Warnung zu unterdrücken, sollte man den User, unter dessen Account der Webserver läuft, zu den "trusted users" in der sendmail-Konfiguration hinzufügen.

16. Datenbanken

16.1. Wie kann ich mehr über SQL lernen?

Keywords: SQL | Datenbank | lernen | Tutorial | Buch | MySQL | PostgreSQL

Antwort von Kristian Köhntopp

Von Markt und Technik gibt es das Buch "SQL in 21 Tagen". Den vollständigen Text kann man bei IT-4-YOU. online lesen. Man kann das Buch auch kaufen.

Guido Stepken hat sein MySQL Datenbankhandbuch auf seiner Website zum Online-Lesen oder zum Download bereitgestellt. (Dieser Text stammt von September 1999 und ist nicht mehr auf Stand).

Das Buch "PostgreSQL", welches bei Addison-Wesley erschienen ist, kann auch online gelesen werden.

Ein englischsprachiges Einsteiger-Tutorial findet sich unter www.sqlcourse.com (Teil 1, behandelte Themen von "Table basics" über "Inserting/Updating/Deleting" bis "Advanced Queries"). Teil 2 nimmt sich folgende Themen vor: "Aggregate Functions", "GROUP BY/HAVING/ORDER BY", "Table Joins" etc. Nettes Special: Ein Online-SQL-Interpreter.

Für MySQL-spezifische Fragen gibt es eine eigene deutschsprachige Newsgroup: de.comp.datenbanken.mysql (FAQ dieser NG)

16.2. Wieso kann ich mehrere, durch Semikolon getrennte Statements nicht ausführen?

Keywords: SQL | Datenbank | Semikolon | phpMyAdmin

Antwort von Kristian Köhntopp

SQL kennt keine Mehrfachstatements. Einige SQL-Frontends (der MySQL-Kommandoprozessor, phpMyAdmin) kennen Mehrfachstatements, die sie manuell in einzelne Anweisungen zerlegen und nacheinander an den Datenbankserver senden. PHP selbst macht dies nicht. Man muss seine Statements manuell zerlegen und einzeln nacheinander absenden.

Um ein Statement zu zerlegen, ist es nicht ausreichend, auf dieses Statement einfach explode() anzuwenden. Beispiel:

INSERT INTO table VALUES('foo;bar');

Wie es richtig geht, kann man im Code von phpMyAdmin nachlesen. Die relevante Stelle ist die Funktion split_sql() in der Datei db_readdump.php.

16.3. Ist es sinnvoll, Bilder in einer Datenbank abzulegen?

Keywords: SQL | Datenbank | Bild | Download | Performance | BLOB

Antwort von Kristian Köhntopp

Aus irgendeinem Grund scheinen viele Leute zu glauben, dass es Bilddaten adeln würde, wenn man sie in eine Datenbank stopft.

Wenn man die Bilddaten selbst in der Datenbank ablegt, hat dies den Vorteil, dass keine broken links auftreten können, weil ja die Bilder selbst genauso wie die Links auf die Bilder aus der Datenbank erzeugt werden. Liegen die Bilddaten dagegen im Dateisystem und die Datenbank enthält nur Pfadnamen, dann ist es problemlos möglich, dass jemand die Dateien umbenennt, ohne diese Änderung in der Datenbank nachzuführen und umgekehrt. Leider ist es speziell bei MySQL so, dass keinerlei Mechanismen vorhanden sind, die die referentielle Integrität der Datenbank sicherstellen, sodass diese Sicherheit nicht wirklich gegeben ist.

Dazu kommen noch eine Reihe von weiteren Nachteilen:

  • Wenn man die Bilddaten selbst in der Datenbank speichert, dann muss man für jedes Bild in einer Webseite ein Script starten. Das bedeutet, für eine Seite wie

    index.php:
    
    <html>
    <body>
    <h1>Bla</h1>
    <img src="sendimage.php?img=geniales_logo.gif" />
    </body>
    </html>
    

    muss nicht nur das Script index.php gestartet werden, um das HTML zu generieren, sondern für jedem Image-Tag auf der Seite muss ein Script sendimage.php gestartet werden, das eine Datenbankverbindung aufmacht und das Bild aus der Datenbank fischt. Wenn CGI PHP verwendet wird, ist der Overhead noch viel größer, denn hier muss für jedes Bild ein 800 kB großer PHP-Prozess erzeugt und gestartet werden.

    Legt man dagegen die Bilder als Dateien im Dateisystem ab, kann man mit der Static Page Engine des Webservers oder gar einem spezialisierten Bilder-Webserver arbeiten und ist um ca. den Faktor 10 effizienter.

  • MySQL kann BLOBs (binary large objects) nicht fragmentarisch bearbeiten, d.h. es ist nicht möglich, ein BLOB in kleinen Teilstücken aus der Datenbank zu holen oder den hinteren Teil eines BLOBs zu holen, ohne die Bytes davor zu lesen. Obendrein ist der Sendepuffer von MySQL für BLOBs begrenzt groß, sodass nicht beliebig große BLOBs in der Datenbank abgelegt werden können.

  • Viele Datenbanken werden sehr ineffizient, wenn vergleichsweise große BLOBs zusammen mit anderen, sehr kleinen Objekten in derselben Tabelle gespeichert werden oder wenn eine Tabellenzeile mehr als ein BLOB enthält.

Wie man Bilder in einer MySQL-Datenbank speichert, wird im Artikel Wie kann ich Bilder in einer MySQL-Datenbank speichern? beschrieben.

16.4. Windows: Jeder Zugriff auf meine Datenbank dauert eine halbe Minute!

Antwort von Kristian Köhntopp

Eine Komponente Deines Netzes versucht, aus IP-Nummern per gethostbyaddr() auf den Hostnamen zu schließen und findet keinen Domain Name Server (DNS). Die Verarbeitung des Requests wird erst nach dem DNS-Timeout fortgesetzt.

Sorge dafür, dass ein DNS-Server mit korrektem Reverse Lookup verfügbar ist oder sorge dafür, dass keine Reverse Lookups gemacht werden. Dazu musst Du zunächst einmal die Komponente identifizieren, die die Lookups macht.

16.5. Wie kann ich meine Datenbankperformance steigern?

Antwort von Kristian Köhntopp

Bei jeder Art von Performancetuning ist das Wichtigste zunächst einmal eine Messung. Es kommt ganz entscheidend darauf an, als erstes festzustellen, was denn genau langsam ist, bevor man sich daran macht, die Dinge zu verändern. Wenn ein Script mit Datenbankzugriff zu langsam ist, dann kann dies mehrere Ursachen haben:

Die Datenbank steht offsite oder ist nur langsam erreichbar.

Wenn die Datenbank nicht auf derselben Maschine läuft wie der Webserver, dann findet die Kommunikation zwischen Datenbank-Client und Server nicht mehr über schnelle Kommunikationsmethoden wie shared memory oder UNIX Domain Sockets statt, sondern über eine TCP/IP-Verbindung, die eine wesentlich geringere Kapazität und wesentlich höhere Latenzzeiten hat. Dies hat besonders fatale Auswirkungen, wenn die Datenbank und der Webserver durch ein langsames Netzwerk getrennt sind (Umlaufzeiten für Pakete von 10ms und mehr) oder wenn die Netzwerkbandbreite eingeschränkt ist (8 KB/sec und weniger).

Hier kommt es ganz entscheidend darauf an, die Anzahl der Anfragen pro Seitenaufbau zu vermindern und die Menge der übertragenen Daten zu verringern. Die Anzahl der Abfragen lässt sich dadurch vermindern, dass man SQL JOIN-Operationen statt vieler Abfragen verwendet. Ein typisches, falsches Konstrukt ist

// Liste der Treffer bestimmen
$result=do_database_query("select id from tabelle where $bedingung");

// Treffer anzeigen
foreach ($result as $k => $v) {
  $detail = do_detail_query("select * from tabelle2 where id =$v");
  show_detail($detail);
}

Dieser Code generiert eine Masse von Queries nacheinander. Für jede einzelne Query wird ein Umlauf zur Datenbank und zurück notwendig und so summieren sich diese Umlaufzeiten zu gigiantischen Wartezeiten beim Seitenaufbau. Viel geschickter ist stattdessen

// Treffer bestimmen
$detail = do_database_query("select * from tabelle, tabelle2
 where ( $bedingung ) and tabelle.id = tabelle2.id");
foreach ($detail as $k => $v) {
  show_detail($v);
}

Dies liefert die gewünschten Daten mit einer einzigen Query.

Die Datenbank hat hohe Verbindungsaufbaukosten und es wird CGI PHP verwendet.

Einigen Datenbanken, wie z. B. MySQL, macht es nichts aus, Datenbanklinks zu öffnen und wieder zu schließen. Andere Datenbanken, wie z. B. Oracle, starten bei jedem Connect einen eigenen Clientprozess. Dies ist ein sehr aufwendiger Vorgang. Wenn CGI PHP verwendet wird, dann endet der CGI Interpreter am Ende jeder Seite und mit dem Interpreter werden auch alle geöffneten Dateihandles und damit auch alle Datenbankverbindungen geschlossen - der Clientprozess der Datenbank endet und muss für eine neue Seite neu geladen und gestartet werden.

In solchen Fällen ist die Verwendung eines PHP-Interpreters als Modul vorzuziehen, weil in dieser Konfiguration die mit pconnect() geöffneten Links über die Lebensdauer einer PHP-Seite hinaus gehalten und auf neuen Seiten wiederverwendet werden können.

Die Queries in der Datenbank sind nicht effizient.

Alle Datenbanken haben Werkzeuge zur Analyse von Anfragen. In MySQL ist dies das EXPLAIN Kommando, in Oracle ist es EXPLAIN PLAN. Die Ausgabe dieser Kommandos sollte man in jedem Fall verstehen lernen und zu Rate ziehen. Nur so kann man erkennen, ob Indizes zur Beschleunigung der Query verwendet werden, ob die Typen von Key und Foreign Key zueinander kompatibel sind und ob die Datenbank die richtige Tabelle als treibende Tabelle in einem JOIN verwendet.

16.6. Wie kann ich zwei Tabellen miteinander verknüpfen?

Antwort von Kristian Köhntopp

Man kann dies mit Hilfe einer JOIN-Operation tun. Diese ist im Kapitel 7.20 des MySQL-Handbuches beschrieben.

Wenn die Tabellen artikel und email als Primärschlüsselfelder artikel.KundenID und email.eid haben und artikel mit email über den Fremdschlüssel email.KundenID verknüpft ist, dann kann man einen Equi-JOIN mit dem folgenden Statement formulieren:

mysql> select * from artikel;
+----------+
| KundenID |
+----------+
|        1 |
|        2 |
|        3 |
+----------+
3 rows in set (0.00 sec)

mysql> select * from email;
+-----+----------+
| eid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        2 |
|   3 |        3 |
+-----+----------+
3 rows in set (0.00 sec)

mysql> select a.KundenID as aid,
     >        e.eid as eid,
     >        e.KundenID as e_aid
     >   from artikel as a,
     >     email as e
     > where a.KundenID = e.KundenID;
+-----+-----+-------+
| aid | eid | e_aid |
+-----+-----+-------+
|   1 |   1 |     1 |
|   2 |   2 |     2 |
|   3 |   3 |     3 |
+-----+-----+-------+
3 rows in set (0.01 sec)

In keinem Fall können in den herangejointen Tabellen Nullwerte enthalten sein.

Diese Operation ist dann effizient, wenn a.KundenID und t.KundenID denselben Typ haben, und auf auf a.KundenID und t.KundenID ein UNIQUE INDEX oder ein INDEX liegen. In MySQL ist ein PRIMARY KEY immer auch ein UNIQUE INDEX.

Wenn man optionale Werte hat, dann kann man keinen symmetrischen Join (Equijoin) mehr machen, sondern muss einen asymmetrischen Join (Left Join) durchführen. Dadurch können auf der rechten Seite Nullwerte entstehen:

mysql> select * from telefon;
+-----+----------+
| tid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        3 |
+-----+----------+
2 rows in set (0.00 sec)


Equijoin (es fehlt KundenID 2, weil keine
Telefonnummer definiert ist):

mysql> select a.KundenID as aid,
     >        e.eid as eid,
     >        e.KundenID as e_aid,
     >        t.tid as tid,
     >        t.KundenID as t_aid
     >   from artikel as a,
     >        email as e,
     >        telefon as t
     > where a.KundenID = e.KundenID
     > and a.KundenID = t.KundenID;
+-----+-----+-------+-----+-------+
| aid | eid | e_aid | tid | t_aid |
+-----+-----+-------+-----+-------+
|   1 |   1 |     1 |   1 |     1 |
|   3 |   3 |     3 |   2 |     3 |
+-----+-----+-------+-----+-------+
2 rows in set (0.02 sec)


Left Join (generiert Nullwerte):

mysql> select a.KundenID as aid,
     >        t.tid as tid,
     >        t.KundenID as t_aid
     >   from artikel as a left join telefon as t
     >        on a.KundenID = t.KundenID;
+-----+------+-------+
| aid | tid  | t_aid |
+-----+------+-------+
|   1 |    1 |     1 |
|   2 | NULL |  NULL |
|   3 |    2 |     3 |
+-----+------+-------+
3 rows in set (0.00 sec)


Unterschiedliche Counts:

mysql> select count(a.KundenID) as acount,
     >        count(t.KundenID) as tcount
     >   from artikel as a left join telefon as t
     >        on a.KundenID = t.KundenID;
+--------+--------+
| acount | tcount |
+--------+--------+
|      3 |      2 |
+--------+--------+
1 row in set (0.01 sec)

Die Tabelle a ist hier die aufspannende Tabelle, die Tabelle t ist die aufgespannte Tabelle. An den Stellen, an denen t keine zu a passenden Werte hat, tauchen Nullwerte in t auf. Da die Relation nun nicht mehr symmetrisch ist, muss man zwischen a.KundenID und t.KundenID unterscheiden. Insbesondere sind die count()-Werte beider Spalten unterschiedlich.

Da a.KundenID und t.KundenID unterschiedlich sind, muss man auch zwingend mit qualifizierten Namen arbeiten und kann nicht mehr einfach KundenID schreiben.

Ein gemischter Join verwendet Equijoins und Left Joins, wie es gerade passt:

mysql> select a.KundenID as aid,
     >        e.eid as eid, e.KundenID as e_aid,
     >        t.tid as tid, t.KundenID as t_aid
     >   from artikel as a,
     >        email as e left join telefon as t
     >        on a.KundenID = t.KundenID
     >  where a.KundenID = e.KundenID;
+-----+-----+-------+------+-------+
| aid | eid | e_aid | tid  | t_aid |
+-----+-----+-------+------+-------+
|   1 |   1 |     1 |    1 |     1 |
|   2 |   2 |     2 | NULL |  NULL |
|   3 |   3 |     3 |    2 |     3 |
+-----+-----+-------+------+-------+
3 rows in set (0.01 sec)

16.7. Was ist Aggregation? Was ist GROUP BY?

Keywords: SQL | Datenbank | group by

Antwort von Kristian Köhntopp

Mit Hilfe der GROUP BY-Clause kann man in SQL Daten aggregieren, also Äquivalenzklassen über den gefundenen Elementen bilden und mit den so gefundenen Teilmengen arbeiten.

Gegeben sei eine Menge von Tupeln, etwa (1, 2), (1, 3), (2, 3), (2, 2), (3, 17), (2, 21). Man kann diese Menge jetzt in Teilmengen unterteilen, das wäre dann in der Mathematik eine Relation. Die Elemente, die gemeinsam in einer Teilmenge stehen, stehen dann in einer Relation zueinander.

Eine Relation ist zum Beispiel kleiner als x. Nimmt man zum Beispiel die Menge N (natürliche Zahlen) und die Relation kleiner als 10, dann teilt diese Relation die Menge N in zwei Teilmengen: Die Menge der natürlichen Zahlen, die die Relation erfüllen (also die Zahlen 0, 1, 2, 3, ..., 9) und die Menge der natürlichen Zahlen, die die Relation nicht erfüllen (die Zahlen 10, 11, ...).

Ebenso kann man eine Äquivalenzrelation definieren. Eine solche Relation definiert mehrere Teilmengen und die Elemente einer Teilmenge sind gleich. In N mit == als Relation ist das witzlos, da die Teilmengen dann einelementig sind, aber mit den o.a. Tupeln kann man ein sinnvolles Beispiel definieren, wenn man als Äquivalenzrelation Gleichheit des ersten Elementes definiert. Man bekommt dann die folgenden Teilmengen:

Die Menge 1 == { (1, 2), (1, 3) }
Die Menge 2 == { (2, 3), (2, 2), (2, 21) }
Die Menge 3 == { (3, 17) }

Angenommen, die Tupel seien eine Tabelle

CREATE TABLE beispiel (
  x integer,
  y integer
);

dann würde man die o.a. Tupel als

INSERT INTO beispiel (x, y) values (1, 2);
INSERT INTO beispiel (x, y) values (1, 3);
INSERT INTO beispiel (x, y) values (2, 3);
INSERT INTO beispiel (x, y) values (2, 2);
INSERT INTO beispiel (x, y) values (2, 21);
INSERT INTO beispiel (x, y) values (3, 17);

definieren und bekäme die Äquivalenzrelation aus dem Beispiel als

SELECT x AS mengenname FROM beispiel GROUP BY x;
+------------+
| mengenname |
+------------+
|          1 |
|          2 |
|          3 |
+------------+
3 rows in set (0.01 sec)

d.h. die Tupel (x, y) mit gleichem x bilden jeweils eine Menge. Wir sehen uns von diesen Tupeln jeweils nur die x an.

Die Mächtigkeit der Mengen 1, 2 und 3 kann man mittels count() bestimmen:

SELECT x AS mengenname, COUNT(x) AS maechtigkeit
  FROM beispiel
  GROUP BY x;
+------------+--------------+
| mengenname | maechtigkeit |
+------------+--------------+
|          1 |            2 |
|          2 |            3 |
|          3 |            1 |
+------------+--------------+
3 rows in set (0.00 sec)

Man kann sich auch das Tupel (x, y) wieder ausgeben lassen:

SELECT x, y FROM beispiel GROUP BY x;
+------+------+
| x    | y    |
+------+------+
|    1 |    2 |
|    2 |    3 |
|    3 |   17 |
+------+------+
3 rows in set (0.00 sec)

MySQL wählt hier irgendein y, da ja per Definition alle (x, y) innerhalb einer Teilmenge gleich sind und jedes Element der Teilmenge daher als Repräsentant der Teilmenge gewählt werden kann.

16.8. Was ist der Unterschied zwischen connect und pconnect?

Antwort von Kristian Köhntopp

In PHP bieten die meisten Datenbanken zwei connect()-Funktionen an: Eine gewöhnliche und eine pconnect()-Funktion. Verwendet man CGI PHP, unterscheiden sich beide Funktionen nicht.

Verwendet man das PHP-Modul, werden die mit einem connect() hergestellten Datenbankverbindungen am Ende der Seite geschlossen. Mit pconnect() hergestellte Verbindungen bleiben jedoch geöffnet. Dies dient einzig und alleine dazu, das ständige Öffnen und Schließen von Netzwerkverbindungen zu vermeiden, denn der Verbindungsaufbau ist bei einigen Datenbanken (etwa Oracle) sehr aufwendig.

Es ist daher empfehlenswert, in jedem Fall die pconnect()-Variante zu verwenden (aber: Vergleiche Webserver verstehen und tunen. Es können sehr viele offene Datenbankverbindungen entstehen). Siehe auch: PHP Manual, Persistente Datenbankverbindungen .

16.9. Wie kann ich mein Datenbankpasswort gegen Spionage sichern?

Keywords: SQL | Datenbank | Sicherheit | Passwort

Antwort von Kristian Köhntopp

Viele PHP-Scripte enthalten Connectinformationen mit Passworten und anderen wichtigen Daten, die nicht in falsche Hände fallen dürfen. Um diese Scripte gegen Zugriff von außen (über den Webserver) zu sichern, gibt es folgende Maßnahmen:

  • Die beste Lösung besteht darin, die Connectinformation in einer Include-Datei außerhalb des Webdateibaumes zu lagern. Wenn der eigene Webserver zum Beispiel die Document-Root /home/www/servers/www.kundenname.de/pages hat, man eigene Dateien jedoch schon ab der Ebene /home/www/servers/www.kundenname.de ablegen darf, dann ist es sinnvoll, sich ein Includeverzeichnis /home/www/servers/www.kundenname.de/php zu definieren und dort die Includedateien mit dem Loginnamen und Passwort abzulegen.

    Da dieses Include-Verzeichnis außerhalb der Document-Root liegt und keine URL hat, kann es auch nicht über den Webserver abgerufen werden.

  • Die nächstschlechtere Lösung besteht darin, ein Verzeichnis unterhalb der Document-Root anzulegen und dieses Verzeichnis mit einem Passwort zu sichern. Niemand, auch der Eigentümer des Servers, muss auf dieses Verzeichnis per HTTP zugreifen können, sondern Wartung erfolgt in der Regel per FTP - es kann also auch ein ungültiges Passwort definiert werden.

  • Die schlechteste Lösung besteht darin, diese Include-Datein mit der Endung .php im normalen Dokumentenbaum zu hinterlegen und darauf zu vertrauen, dass Dateien mit dieser Endung immer geparsed werden. Während dies im Normalbetrieb immer der Fall ist, braucht der Webserver nur einmal ohne PHP-Modul gestartet zu werden und die Datei wird im Klartext ausgeliefert (näheres in "Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?").

Die connect()-Funktionen in PHP verlangen alle, dass das Datenbankpasswort im Klartext angegeben wird. Das bedeutet, dass das Passwort entweder im PHP-Code im Klartext angegeben ist oder vom Code in Klartext entschlüsselt werden kann. Wenn jemand die Dateien mit den Passworten oder dem Entschlüsselungscode lesen kann, dann bedeutet dies auch, dass das Klartextpasswort dieser Person bekannt wird. Die betreffende Person braucht den Entschlüsselungscode nicht zu verstehen - sie braucht ihn nur auszuführen und er wird zwangsläufig das Klartextpasswort passend für die Connect-Funktion liefern müssen.

Daraus folgt, dass ein Schutz der Datenbankpassworte nur durch einen Schutz der entsprechenden Quelltextdateien möglich ist. Es ist Aufgabe des Hosting-Environments beim Provider, diesen Schutz zu bieten, indem entweder Zugriffsrechte an den Dateien entsprechend gesetzt sind oder indem sogar eine virtuelle Dateiumgebung mit chroot() eingerichtet wird.

16.10. MySQL oder PostgreSQL?

Keywords: SQL | Datenbank | MySQL | PostgreSQL | Vergleich

Antwort von Kristian Köhntopp

von Kristian Köhntopp, Lutz Donnerhacke und Sebastian Bergmann.

MySQL ist eine sehr populäre Datenbank, die sich vor allen Dingen durch Geschwindigkeit und geringen Speicherverbrauch sowie durch einfache Handhabung auszeichnet. MySQL verfügt über eine sehr gute Dokumentation, ist auch für die Windows-Plattform verfügbar und seit Ende Juni 2000 unter der GPL frei verfügbar. Seit Version 3.23.16 gibt es experimentellen Support für Transaktionen auf Basis der Sleepycat DB3-Bibliothek, aber noch keine Trigger oder Rules.

Das Buch MySQL von Paul DuBois (englische Version) erläutert die Datenbank umfassend und enthält eine Reihe von allgemeinen und speziell auf MySQL bezogene Optimierungstips. Mit phpMyAdmin von Tobias Ratschiller existiert eine einfach zu bedienende, in PHP geschriebene Administrationsoberfläche für MySQL.

PostgreSQL ist der großteils geglückte Versuch, eine freie Implementation von SQL92 aus einem SQL-fremden Konzept (Ingres) abzuleiten. Dazu gehören Transaktionen in verschiedenen Abschottungsgraden, Subselects, eigene Datentypen, Operatoren und Aggregatfunktionen, Trigger, Rules ('Trigger', die in die Optimierungsplanung eingehen) und Views. Es ist somit möglich, dass die Datenbank die Konsistenz des Datenbestandes aus sich heraus erzwingt und so Direktzugriffe ohne korrigierende Frontends gestattet. Die Geschwindigkeit von PostgreSQL ist dadurch allerdings vermindert. Verzichtet man auf Datanbankkonsistenz auf der Platte im Falle von OS-Abstürzen, so wird PostgreSQL deutlich schneller. Ebenso wie MySQL fehlen auch PostgreSQL noch einige elementare Funktionen zur vollen SQL92-Kompatibilität; im Falle von PostgreSQL sind dies zum Beispiel Outer Joins. Die Möglichkeiten von PostgreSQL machen es notwendig, die Datenbank vorab sorgfältig zu planen.

Inzwischen existiert mit phpPgAdmin eine von Dan Wilson nach PostgreSQL portierte Version von phpMyAdmin.

Eine ausführliche Gegenüberstellung der beiden Datenbanken bietet MySQL im Vergleich mit PostgreSQL aus demMySQL-Handbuch.

16.11. Wie komme ich bei meinem Provider an die Datenbank?

Antwort von Kristian Köhntopp

Solche Fragen klärt man am Besten mit dem Telefonsupport des betreffenden Providers.

  • Puretec 1&1:

    mysql_connect("dbxy.puretec.de", "Nummer", "Passwort");
    

    Dabei steht xy für die Nummer des Datenbankservers, die im 1&1-Online-Konfigurationsmenü unter "Zugangsdaten -> Datenbank-Zugriff -> Host" zu finden ist.

  • Strato:

    mysql_connect("rdbms.strato.de", "www.domainname.de", "Datenbankpasswort");
    

    Eine neue Datenbank und das dazugehörige Passwort lassen sich im Kundenbereich einrichten.

16.12. Wie kann ich auf einen ODBC-Server (MSSQL, Access) zugreifen?

Keywords: SQL | Datenbank | ODBC | Access | Windows

Antwort von Kristian Köhntopp

In Windows kann man einfach den mitgelieferten ODBC-Treiber verwenden. Eine Beschreibung befindet sich im PHP-Manual .

In Unix kann man für den Zugriff auf einen Microsoft SQL Server den Sybase-CT Treiber verwenden, der ein weitgehend kompatibles Protokoll verwendet. Sybase bietet eine frei verfügbare Version der benötigten Bibliotheken für Linux zum Download an.

Alternativ kann man auch einen kommerziellen ODBC-Treiber für Unix verwenden, etwa den Treiber von OpenLink Software oder den iODBC-Treiber, der dem Adabas-Paket für Suse Linux beiliegt.

16.13. Wieso wird aus " plötzlich \" und wie geht das wieder weg?

Antwort von Johannes Frömter

Das sind Escapes, die vor bestimmten Sonderzeichen stehen, um diese zu "entschärfen". Verantwortlich für dieses Verhalten ist die Funktion magic_quotes von PHP, die üblicherweise in der php.ini eingestellt wird. Dabei gilt magic_quotes_gpc für Daten, die per GET, POST oder COOKIE übergeben werden und magic_quotes_runtime für Daten, die aus Datenbanken, Dateien oder anderen externen Quellen kommen. Escaped werden ' (single quote), " (double quote), \ (backslash) und NUL (das Null-Byte).

Um die Escape-Zeichen wieder zu entfernen, benutzt man stripslashes() ; manuell hinzufügen kann man sie mittels addslashes() .

Die Konfiguration von magic_quotes kann man an verschiedenen Stellen beeinflussen:

php.ini:
magic_quotes_runtime = on|off
magic_quotes_gpc     = on|off

.htaccess, httpd.conf:
php_flag magic_quotes_runtime on|off
php_flag magic_quotes_gpc     on|off

.php
ini_set("magic_quotes_runtime", 0|1);
// magic_quotes_gpc geht hier nicht

In Verbindung mit Sybase-Datenbanken (d.h. bei zusätzlich gesetzter Option magic_quotes_sybase ) gibt es eine Besonderheit: Hier werden single quotes nicht mit einem Backslash, sondern mit einem weiteren single quote escaped.

16.14. Warum soll ich nicht SELECT * schreiben?

Keywords: SQL | Select | Wildcard | Performance

Antwort von Johannes Frömter

Bei der SQL-Anweisung SELECT * FROM ... muss das Datenbank-Management-System (DBMS) alle Spalten der betreffenden Datensätze selektieren, auch wenn in der anschließenden Verarbeitung nur ein Teil davon wirklich gebraucht wird. Das ist langsam und schlicht und einfach unsinnig, und die unnötigen Spalten verhindern unter Umständen, dass der integrierte Optimizer die Query effizient ausführen kann.

Selbst wenn alle Spalten tatsächlich benötigt werden, sollten sie separat aufgeführt werden, weil

  • die Tabelle nachträglich erweitert werden könnte, die neue(n) Spalte(n) (im worst case ein BLOB!) nach der Abfrage aber nicht gebraucht werden

  • die Reihenfolge der Spalten bei der Ausgabe sonst undefiniert ist (bei den meisten Datenbanken ist es die Reihenfolge der Spaltendefinition bei der Anlage der Tabelle). Diese Reihenfolge könnte sich ändern (z.B. durch Einspielen eines Backups nach Erweiterung der Tabelle, durch eine neue Version des DBMS, etc.)

  • die Spalten sonst möglicherweise keinen vernünftigen oder eindeutigen Namen haben. Führt man die Spalten einzeln an, kann man mittels AS einen Namen (Alias) vergeben: SELECT p.pers_p_nr AS personalnummer FROM personal p ORDER BY personalnummer

  • dadurch im Script quasi automatisch dokumentiert wird, welche Spalten anschließend verarbeitet werden

Im MySQL-Manual wird in den Beispielen der Einfachheit halber fast immer SELECT * verwendet. Daran darf man sich jedoch für die Praxis kein Beispiel nehmen, sagt auch das Handbuch explizit - Zitat: "You should NEVER, in an application, use SELECT * and retrieve the columns based on their position, because the order in which columns are returned CANNOT be guaranteed over time; A simple change to your database may cause your application to fail rather dramatically."

Auch für INSERT gilt: immer alle Spaltennamen angeben! Statt INSERT INTO tabelle VALUES (1, 2, 3) ist also INSERT INTO tabelle (spalte1, spalte2, spalte3) VALUES (1, 2, 3) zu schreiben.

16.15. Wie bekomme ich den letzten Datensatz aus der Tabelle?

Keywords: SQL | Select | erster | letzter | Datensatz | Eintrag | Zeile

Antwort von Johannes Frömter

In einer relationalen Datenbank gibt es keine Reihenfolge, also keine "letzte Zeile", kein "oben" oder "unten". Jedwede definierte Reihenfolge entsteht erst beim Selektieren von Datensätzen und der Sortierung nach irgendeinem Sortierkriterium.

Um also den "ersten" oder "letzten" Datensatz selektieren zu können, muss die Tabelle eine Spalte vom Typ TIMESTAMP haben, die manuell oder automatisch auf das Eintrags- bzw. Änderungsdatum gesetzt wird. Anhand dieser Spalte kann das Ergebnis sortiert werden (ASC sortiert aufsteigend, DESC absteigend). Mit LIMIT wird das Ergebnis dann auf den ersten (oder die ersten n) Datensätze beschränkt:

SELECT xy FROM tabelle
ORDER BY datum DESC
LIMIT 1

Wenn das DBMS Subselects zulässt (für MySQL ist dies ab Version 4.x geplant), kann man alternativ auch folgendes schreiben:

SELECT xy FROM tabelle
WHERE datum = (SELECT MAX(datum) FROM tabelle)

Damit diese Selects effektiv vonstatten gehen, muss auf die Timestamp-Spalte ein Index gelegt werden. Die Verwendung eines Primärschlüssel- statt Timestamp-Feldes ist nicht empfohlen.

16.16. Meine IDs haben Lücken - wie vergebe ich sie neu?

Antwort von Johannes Frömter

Die IDs einer AUTO_INCREMENT-Spalte haben den Sinn, jede Zeile in der Tabelle, d.h. jeden Datensatz, eindeutig zu kennzeichnen. Durch Löschen von Datensätzen entstehen zwar "Lücken" in der Nummerierung, da für neue Datensätze stets eine unverbrauchte ID vergeben wird. An dieser vermeintlichen "Unordnung" sollte man aber auf keinen Fall etwas ändern - dies würde dem Prinzip der Eindeutigkeit zuwiderlaufen.

Der höchste Wert der ID hat auch nichts mit der Anzahl der Datensätze in der Tabelle zu tun. Diese ermittelt man mit einer SQL-Funktion: SELECT COUNT(*) AS anzahl FROM tabelle

MySQL kennt (noch) keine referentielle Integrität, deshalb muss man selbst dafür sorgen, dass Verknüpfungen zwischen Tabellen (Primary/Foreign Keys) konsistent bleiben. Deshalb: Finger weg von den IDs! Bei geeigenter Wahl des Datentyps braucht man auch keine Sorge zu haben, dass Zahlenraum knapp werden könnte. Siehe auch: "Wie lege ich den Initialwert des auto_increment fest? Läuft dieser Wert über?".

16.17. Meine Datenbankabfrage funktioniert nicht

Keywords: SQL | Abfrage | Datenbank | Fehler | Error

Antwort von Clemens Koppensteiner

Fehlermeldungen

Als erster Anhaltspunkt sind die Fehlermeldungen der verwendeten Datenbank gut geeignet. Für viele Datenbanken bietet PHP hierfür eigene Funktionen. Dies sind zum Beispiel mysql_error() , pg_last_error() , ora_error() , odbc_errormsg() und sybase_get_last_message() .

Hier ein Beispiel für MySQL

function mysql_errorhandler($problem, $query = "")
{
  echo "<font color='#FF0000'><b>Datenbankfehler:</b></font><br />\n";
  echo "Problem: $problem <br />\n";
  if($query != "")
  {
    echo "Query: $query <br />\n";
  }
  echo "MySQL: " . mysql_errno() . " - " . mysql_error() . "<br /><br />\n";
}

// Verbindung zum Datenbankserver herstellen
if(!$db = @mysql_connect("host", "user", "password"))
{
  mysql_errorhandler("Verbindungsaufbau gescheitert.");
}

// Datenbank auswählen
if(!@mysql_select_db("database"))
{
  mysql_errorhandler("Auswahl der Datenbank gescheitert.");
}

// Beispiel für ein SQL-Statement
$query = "SELECT * FROM table WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
  mysql_errorhandler("Datenbankabfrage gescheitert", $query);
}

Klassen zur Datenbankabstraktion (z.B. PEAR::DB) können das Fehler-Handling wesentlich vereinfachen.

Reservierte Wörter

Bestimmte Wörter sollten nicht als Namen von Feldern vorkommen, da sie andere Bedeutungen haben. Diese "Reserved Words", oder auch "Keywords", sollten im Handbuch der verwendeten Datenbank aufgelistet sein.

Bei MySQL und PostgreSQL kann man das Problem umgehen, indem man den Feldnamen in Anführungszeichen einschließt:

SELECT * FROM users WHERE 'group' = 2

Zuständige Newsgroups

Falls das alles nichts hilft, frage in den zuständigen Newsgroups nach. Das sind unter anderem de.comp.datenbanken.misc, de.comp.datenbanken.mysql und die englischsprachigen Gruppen comp.databases.*

16.18. Wie kann ich bösartigen Code in SQL-Abfragen unterbinden?

Keywords: SQL | Injection | Quoting

Antwort von Alex Kiesel

In nahezu allen Einsatzgebieten einer Datenbank wird die Datenbank durch ein bestimmtes Benutzerinterface (z.B. über das Web) abgefragt und auch gefüllt. Oft darf ein bestimmter Benutzer nicht alles sehen, was sich in der Datenbank befindet - und schon gar nicht ändern oder löschen. Falsch programmierte Scripte ermöglichen jedoch genau dies - dabei ist ein Schutz gegen die als SQL-Injection bekannte Technik gar nicht so schwer. Auf die Prüfung der übergebenen Parameter kommt es an. (Siehe dazu auch Prüfe importierte Parameter. Traue niemandem).

Zunächst mal ein Beispiel, wie eine solche SQL-Injektion aussehen könnte: in einer Benutzerverwaltung darf ein User genau sein eigenes Passwort ändern. Wir haben also die Tabelle "account", die insbesondere die Logindaten des Users und des Admins enthält. Der Benutzer sendet das Passwort-Ändern-Formular in seinem Browser ab und serverseitig wird das neue Passwort in die Datenbank geschrieben:

  $result= $db->update ('
    update account
    set password= "'.$_REQUEST['newpassword'].'"
    where username= "'.$_REQUEST['username'].'"'
  );

Hier wird die Variable $_REQUEST['newpassword'] nicht geprüft - der Benutzer kann eintragen, was er möchte. Mit etwas Trial-and-Error findet der Benutzer heraus, dass er die doppelten Anführungszeichen (") als String-Delimiter benutzen muss; er gibt nun ein Passwort seiner Wahl und als Username userxy" or username="admin. Somit ergibt sich nach der Textersetzung folgendes SQL:

  update account
  set password= "adminsuxx"
  where username="userxy" or username="admin"

Es kommt hier also hochgradig darauf an, niemals fremden Parametern zu trauen. Auf was muss also getestet werden? Parameter, die innerhalb von Quotes als String an die Datenbank übergeben werden sollen, dürfen selbst keine ungeschützten Quotes enthalten. Parameter, die IDs oder Zahlen repräsentieren, dürfen nur Ziffern enthalten. Die beiden Funktionen sichern genau dies, und sorgen zudem dafür, dass NULL-Values korrekt wiedergegeben werden:

  function sqlSafeString($param) {
    // Hier wird wg. der grossen Verbreitung auf MySQL eingegangen
    return (NULL === $param ? "NULL" : '"'.mysql_escape_string ($param).'"');
  }

  function sqlSafeInt($param) {
    return (NULL === $param ? "NULL" : intVal ($param));
  }

(Die genauen Vorgaben zum Schützen von Sonderzeichen hängen vom SQL-Dialekt des RDBMS ab und müssen gegebenenfalls angepasst werden.) Man hätte so oben beschriebenes SQL so umschreiben können:

  $result= $db->update ('
    update account
    set password= '.sqlSafeString ($_REQUEST['newpassword']).'
    where username= '.sqlSafeString ($_REQUEST['username'])
  );

Obiger SQL-Injection-Versuch wäre dann gescheitert (weil keine Zeile auf diesen Usernamen matcht):

  update account
  set password= "adminsuxx"
  where username="userxy\" or username=\"admin\""

Eine Funktion, die komfortable SQL-Manipulation mit automatischem Escaping bietet, wird in String-Quoting bei Sybase vorgestellt; sie ist zwar für Sybase geschrieben worden, lässt sich aber leicht für andere Datenbanksysteme adaptieren.

17. Datenbanken: MySQL

17.1. Kommt MySQL mit mehr als x Datensätzen pro Tabelle klar? Wie stabil ist MySQL?

Antwort von Kristian Köhntopp

MySQL ist als Datenbank äußerst stabil und auch bei großen Datenmengen extrem schnell und effizient. Die Grenzen von MySQL liegen nicht so sehr in der Größe der Tabellen oder der Anzahl von Datensätzen, sondern in der Komplexität der Datenmodelle, die damit implementiert werden kann.

MySQL speichert Daten mit Index in Baumstrukturen. Auf diese Datenstrukturen kann mit logarithmischer Komplexität zugegriffen werden, d.h. für eine Tabelle mit n Datensätzen sind log(b, n) Zugriffe notwendig, bis der gesuchte Datensatz gefunden ist. b ist die Basis des Logarithmus. Wäre b gleich 2, dann wären zum Zugriff auf eine Tabelle mit 1.000 Datensätzen maximal 10, auf eine Tabelle mit 1.000.000 Datensätzen maximal 20 und auf eine Tabelle mit 1.000.000.000 maximal 30 Zugriffe notwendig, um einen beliebigen Zieldatensatz über den Index zu finden. Tatsächlich ist die Basis jedoch nicht 2, sondern weit größer. Sie ist abhängig von der internen Blockgröße der Datenbank und der mittleren Satzlänge in einem Index. Man kann annehmen, dass sie je nach Art der Daten zwischen 20 und 40 liegt. Damit wären zum Finden eines Datensatzes aus 1.000 Datensätzen maximal 3, aus 1.000.000 Datensätzen maximal 5 und aus 1.000.000.000 Datensätzen maximal 7 Vergleiche und Plattenzugriffe notwendig.

Entsprechend sind die Erfahrungen, die mit MySQL berichtet werden: Im Rahmen der Begrenzungen des Betriebssystems kommt MySQL mit beliebig großen Tabellen problemlos klar. Eine Auflistiung der Betriebssystem-spezifischen Dateigrößen-Beschränkungen findest du im MySQL-Handbuch unter Wie groß können MySQL-Tabellen sein?.

Beschränkungen ergeben sich in MySQL aus dem Fehlen bestimmter Eigenschaften wie Erzwingung referentieller Integrität (keine foreign key Prüfungen, siehe dazu auch die Bemerkungen über PostgreSQL) und Transaktionen. Das Fehlen dieser Eigenschaften macht die Implementierung von Datenbankschemata sehr mühsam, die schreibend auf mehr als eine Tabelle zur Zeit zugreifen.

Man kann abschätzen, ob MySQL für eine Aufgabe das passende Tool ist, indem man sich das geplante Datenbankschema und die geplanten Transaktionen auf diesem Schema ansieht, alle n:m und Sternbeziehungen isoliert und dann feststellt, in welchen dieser Beziehungen schreibende Zugriffe notwendig sind, die mehr als eine Tabelle aktualisieren.

MySQL ist geeignet für alle Modelle, die read-mostly sind oder die weitaus überwiegend Schreibzugriffe auf einzelne Tabellen haben. MySQL ist nicht optimal geeignet, wenn ein Modell sehr viele Schreibzugriffe hat, wenn ein Modell mehr als 2 Schreibzugriffe hat, die mehr als eine Tabelle gleichzeitig aktualisieren oder wenn ein Modell zwingend auf referentielle Integrität angewiesen ist, aber mehr als eine Anwendung schreibend auf den Bestand zugreift.

17.2. Wie greife ich auf eine MySQL-Datenbank zu?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion mysql_connect() kann man eine Datenbankverbindung zu einem MySQL-Server aufbauen, um dann mit mysql_select_db() die Datenbank auf dem Server auszuwählen. Das Resultat ist eine Link-ID, die man bei allen anderen MySQL-Funktionen angeben kann.

In der php.ini kann man festlegen, welche Datenbank und welcher Benutzername und welches Passwort die Funktion verwenden soll, wenn diese Angaben beim Funktionsaufruf weggelassen werden, auch wenn es nur untermittelschlau ist, dies zu tun:

; default host for mysql_connect() (doesn't apply in safe mode)
mysql.default_host  =
; default user for mysql_connect() (doesn't apply in safe mode)
mysql.default_user  =
; default password for mysql_connect() (doesn't apply in safe mode)
mysql.default_password  =

; Note that this is generally a *bad* idea to store passwords
; in this file. *Any* user with PHP access can run
; 'echo cfg_get_var("mysql.default_password")' and reveal that
; password!  And of course, any users with read access to this
; file will be able to reveal the password as well.

Kontaktaufnahme mit dem Server und Festlegen der Datenbank:

  $link = mysql_connect("localhost", "ich", "geheim");
  if (!$link)
    die("Kann den Server nicht erreichen.");
  if (!mysql_select_db("meinedatenbank", $link))
    die("Kann die Datenbank nicht anwählen.");

Das Senden einer Anfrage an die Datenbank erfolgt mit Hilfe der Funktion mysql_query() . Diese Funktion liefert einen Result-Identifier, mit dem dann das Ergebnis abgefragt werden kann: Mit der Funktion mysql_num_rows() bestimmt man die Anzahl der Zeilen des Ergebnisses und mit Hilfe der Funktion mysql_fetch_array() kann man die jeweils aktuelle Zeile des Ergebnisses einlesen.

$query = "SELECT * FROM meinetabelle ORDER BY id";
$result = mysql_query($query, $link);
if (!$result)
{
    print mysql_error();
    die("Query $query ist ungültiges SQL.");
}

$zeilen = mysql_num_rows($result);
printf("Das Ergebnis hat %d Zeilen.\n", $zeilen);

while($avar = mysql_fetch_array($result))
    printf("Spalte bla hat den Wert %s\n", $avar["bla"]);

mysql_free_result($result);
mysql_close($link);

17.3. Wie kann ich eine CSV-Datei in MySQL importieren?

Keywords: SQL | Datenbank | MySQL | CSV | Import | infile

Antwort von Kristian Köhntopp

In MySQL gibt es die Anweisung LOAD DATA INFILE zum Importieren von Dateien im CSV-Format in die Datenbank. Diese Anweisung wird vom Datenbankserver ausgeführt, d.h. die Datei muss auf dem Rechner liegen, auf dem der Datenbankserver abläuft und die Datei muss world-readable sein. Man benötigt file_priv, um dieses Kommando ausführen zu können.

Seit Version 3.22.6 von MySQL gibt es die Kommandovariante LOAD DATA LOCAL INFILE zum Importieren von Daten im CSV-Format. Dieses Kommando wird auf dem MySQL-Client (also im PHP-Interpreter) ausgeführt. Die Datei muss also auf dem Rechner liegen, auf dem der Client läuft und durch den Client lesbar sein. Man benötigt keine besonderen Privilegien, um dieses Kommando ausführen zu können. Dies ist die empfohlene Variante des Kommandos, falls die zur Verfügung steht.

Das folgende SQL-Kommando liest eine Datei ein, bei der die Datensätze optional mit Anführungszeichen eingeschlossen sind und durch Semikolons getrennt sind. Vorhandene Datensätze in der Tabelle, die ebenfalls im Import enthalten sind, werden durch den Import überschrieben.

LOAD DATA LOCAL
        INFILE '/home/www/servers/www.servername.de/tmp/import.csv'
        REPLACE
        INTO TABLE tabellenname
        FIELDS
                TERMINATED BY ';'
                OPTIONALLY ENCLOSED BY '"';

Eine vollständige Beschreibung des Kommandos in englischer Sprache ist Bestandteil des MySQL Manuals unter der URL http://dev.mysql.com/doc/mysql/en/load-data.html.

Will man die Daten manuell laden, darf man die Zeile nicht mit explode() zerlegen, weil dies bei Datensätzen versagt, die selbst Kommata enthalten. Stattdessen bietet PHP die Funktion fgetcsv() an.

17.4. Wie kann ich eine CSV-Datei aus MySQL exportieren?

Keywords: SQL | Datenbank | MySQL | CSV | Export | outfile

Antwort von Kristian Köhntopp

Die Umkehrung von LOAD DATA INFILE ist das SELECT INTO OUTFILE, eine Variante des regulären SELECT.

SELECT ...
    INTO OUTFILE '/home/www/servers/www.servername.de/tmp/export.csv'
    FIELDS
            TERMINATED BY ';'
            OPTIONALLY ENCLOSED BY '"'
    FROM ...;

Das Kommando wird die Datei auf dem Rechner anlegen, auf dem der Datenbankserver läuft und die Datei wird dem Benutzer gehören, unter dessen User-ID der Datenbankserver abläuft. Der Datenbankserver wird eine existierende Datei nicht überschreiben. Zur Ausführung des Kommandos ist file_priv notwendig.

Eine vollständige Beschreibung des Kommandos in englischer Sprache gibt es im MySQL Manual.

Wenn man kein file_priv hat, muss man sich stattdessen eine entsprechende Funktion in PHP selber bauen. Dabei ist folgendes zu beachten:

  • In CSV-Dateien sind Datensätze durch Kommata getrennt.

  • In CSV-Dateien sind Datensätze, die Sonderzeichen enthalten, insbesondere solche, die Kommata oder Anführungszeichen enthalten, durch Anführungszeichen einzuschließen.

  • In CSV-Dateien dürfen alle Datensätze in Anführungszeichen eingeschlossen werden.

  • In CSV-Dateien sind in Datensätzen, die Anführungszeichen enthalten, die Anführungszeichen zu verdoppeln.

Eine zweispaltige Tabelle, die die Tupel ( a; 10,4) und (b; Er sagte: "Hallo, Du!" ) enthält, muss nach dem Export also so aussehen:

a,"10,4"
b,"Er sagte: ""Hallo, Du!"""

17.5. Wie kann ich die Datensätze der letzten 2 Wochen listen?

Keywords: SQL | Datenbank | MySQL | timestamp | aktuell

Antwort von Kristian Köhntopp

Ein vollständiges Beispiel folgt. Zunächst die Definition einer einfachen Tabelle mit einigen Werten:

mysql> create table beispiel (
    ->   id integer not null auto_increment primary key,
    ->   nutzlast varchar(80) not null,
    ->   changed timestamp not null );
Query OK, 0 rows affected (0.00 sec)

mysql> insert into beispiel ( nutzlast ) values ( "eins");
Query OK, 1 row affected (0.02 sec)

mysql> insert into beispiel ( nutzlast ) values ( "zwei");
Query OK, 1 row affected (0.00 sec)

mysql> insert into beispiel ( nutzlast ) values ( "drei");
Query OK, 1 row affected (0.00 sec)

mysql> select * from beispiel;
+----+----------+----------------+
| id | nutzlast | changed        |
+----+----------+----------------+
|  1 | eins     | 20000126204514 |
|  2 | zwei     | 20000126204517 |
|  3 | drei     | 20000126204520 |
+----+----------+----------------+
3 rows in set (0.02 sec)

mysql> update beispiel set changed = "20000101123456" where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from beispiel where changed > "20000115000000";
+----+----------+----------------+
| id | nutzlast | changed        |
+----+----------+----------------+
|  1 | eins     | 20000126204514 |
|  3 | drei     | 20000126204520 |
+----+----------+----------------+
2 rows in set (0.02 sec)

mysql> quit
Bye

In diesem Beispiel ist eine Tabelle beispiel angelegt worden, die neben einer Nutzlastspalte mit dem Namen nutzlast auch noch einen automatisch vergebenen Primärschlüssel id und ein automatisch aktualisiertes Änderungsdatum enthält. Wir haben das Änderungsdatum für den Datensatz mit der Nummer 2 künstlich zurückgestellt.

Das folgende Beispielscript listet nun alle Datensätze, die in den letzten 2 Wochen geändert worden sind.

<?php

// Eigene Daten eintragen:
$dbhost = "localhost"; // DB-Server
$dbuser = "root";      // DB-Server Username
$dbpass = "...";       // DB-Server Passwort
$dbname = "...";       // Datenbank Name

// Mit DB-Server und Datenbank verbinden, bei Fehler Script beenden.
$db_link = @mysql_connect($dbhost, $dbuser, $dbpass);

if (! $db_link) {
    die("Fehler beim Verbinden mit Server '$dbhost' als '$dbuser' mit Passwort '$dbpass'.");
}

if (! mysql_select_db($dbname, $db_link)) {
    die("Fehler beim Verbinden mit der Datenbank $dbname");
}

// Zeitgrenze bestimmen (86400 Sekunden = 1 Tag).
$twoweeks = date("YmdHis", time() - 14*86400);

// Query generieren.
$query = "SELECT id,
                 nutzlast,
                 date_format(changed, '%Y-%m-%d %H:%i:%s') as changed
          FROM   beispiel
          WHERE  changed > $twoweeks";

$result = mysql_query($query, $db_link);

// Falls Query fehlerhaft, Script beenden.
if (! $result) {
    die("Fehler bei der Query: $query");
}

// Query senden, Resultat ausgeben.
while($avar = mysql_fetch_array($result)) 
{
    printf("%s - %s - %s\n",
            $avar['id'], $avar['nutzlast'], $avar['changed']);
}

// Speicher wieder freigeben und Verbindung zur DB schließen.
mysql_free_result($result);
mysql_close($db_link);

?>

Der Output des Scripts ist:

1 - eins - 2000-01-26 20:45:14
3 - drei - 2000-01-26 20:45:20

17.6. Wie kann ich eine Tabelle nach IP-Nummern sortieren lassen?

Keywords: SQL | Datenbank | MySQL | IP-Nummer | sortieren

Antwort von Johannes Frömter

Seit MySQL 3.23.15 gibt es die Funktionen INET_NTOA(expr) (number to address) und INET_ATON(expr) (address to number), die einem die Konvertierung abnehmen. Damit ist das Sortieren von IP-Adressen kein Problem mehr:

SELECT ip FROM table ORDER BY INET_ATON(ip);

17.7. Wie lösche ich alle Datensätze, die älter als n Tage sind?

Antwort von Kristian Köhntopp

Die betreffende Tabelle sollte ein Datumsfeld haben, etwa ein selbstaktualisierendes Feld vom Typ TIMESTAMP oder ein manuell aktualisiertes Feld vom Typ DATE. Die folgenden drei Queries löschen jeweils alle Datensätze, die älter als 30 Tage sind, mit steigender Effizienz.

1. DELETE
       FROM kalender AS k
      WHERE (to_days(current_date) - to_days(k.datum)) > 30

2. DELETE
       FROM kalender AS k
      WHERE to_days(k.datum) < to_days(current_date)-30;

3. DELETE
       FROM kalender AS k
      WHERE k.datum < date_add(current_date, interval -30 day)

Die erste Query ist vergleichsweise langsam, denn hier ist die linke Seite der Query ein Ausdruck, der für jede Zeile berechnet werden muss. Der Spaltenname k.datum taucht auf der linken Seite in einer Funktionsanwendung auf, sodass keine Indizes angewendet werden können.

Die zweite Query ist insofern optimiert, als dass der konstante Teil der Rechnung auf die rechte Zeit gebracht werden kann, sodass diese Seite der Ungleichung zu einer Konstanten optimiert werden kann. Die linke Seite der Query ist jedoch noch immer eine Funktionsanwendung, sodass ein full table scan notwendig ist.

Die dritte Query ist durchoptimiert: Hier ist die linke Seite der Ungleichung ein reiner Spaltenausdruck, die rechte Seite zu einer Konstanten optimierbar. Wenn ein INDEX(k.datum) existiert, kann er in dieser Query angewendet werden, um den Zugriff zu beschleunigen.

17.8. Wie kann ich Bilder in einer MySQL-Datenbank speichern?

Antwort von Martin Jansen

Die Vor- und Nachteile dieser Methode werden im Kapitel "Datenbanken" im Abschnitt "Ist es sinnvoll, Bilder in einer Datenbank abzulegen?" diskutiert.

Eine sehr gelungene Anleitung, um Binärdaten (also auch Bilder) in einer MySQL-Datenbank zu speichern, beschreibt Florian Dittmer auf http://www.phpbuilder.com/columns/florian19991014.php3. In einem Artikel bei ZDnet beschreibt Julie Meloni einen Datei-Upload per Formular in die Datenbank inkl. Hinweisen speziell zu MySQL, Microsoft SQL Server und Oracle.

17.9. Wie kann ich einen zufälligen Eintrag aus einer MySQL-Tabelle auswählen?

Keywords: SQL | Datenbank | MySQL | Zufall | random

Antwort von Martin Jansen

Seit MySQL 3.23 steht folgende Syntax zur Verfügung, um einen zufälligen Eintrag aus einer Datenbank-Tabelle auszulesen:

SELECT * FROM tabelle ORDER BY RAND() LIMIT 1

17.10. Ich habe eine Tabelle mit n Einträgen und möchte auf jeder Seite m davon anzeigen

Keywords: SQL | Datenbank | MySQL | blaettern | vor | zurueck | limit

Antwort von Kristian Köhntopp

In MySQL kann man zu diesem Zweck die LIMIT-Direktive verwenden, die m Einträge ab Position s einer geordneten Tabelle anzeigt. In anderen Datenbanken muss man sich eine Zeilennummer definieren und kann dann einen Teil der Tabelle mittels einer BETWEEN-Clause auswählen.

# MySQL
mysql> SELECT * FROM tabelle LIMIT s,m;

Es ist nicht effizient, alle n Datensätze der Tabelle zu selektieren und dann alle Datensätze vor Position s zu überlesen.

Antwort von Daniel T. Gorski

Mit Hilfe eines solchen SQL-Statements kann man sich dann leicht eine Funktion schreiben, die den entsprechenden Ausschnitt der Tabelle anzeigt und Links zum vorhergehenden und folgenden Tabellenausschnitt enthält. Im Folgenden möchten wir hier diese einfache "Blättern"-Funktion realisieren, die uns erlaubt, über die Ergebnisse einer Datenbank-Query vor- und zurück zu browsen.

Es wird davon ausgegangen, dass eine einfache MySQL-Datenbank (deren Name über die Variable $database definiert wird) vorhanden ist. Diese enthält die von uns benötigte Tabelle (Variable $table), über die wir "blättern" wollen.

CREATE DATABASE nameDerDatenbank;
USE nameDerDatenbank;
CREATE TABLE nameDerTabelle (ID int(10) unsigned NOT NULL,
                             INHALT text NOT NULL);

Diese Datenbankstruktur kann z.B. mit phpMyAdmin, einem anderem PHP-Script oder direkt mit dem MySQL-Monitor erstellt werden. Um Ausgabeergebnisse zu erhalten, muss die Tabelle selbstverständlich zuerst mit Inhalt gefüllt werden - an dieser Stelle gehen wir davon aus, dass die Tabelle mehrere Einträge enthält.

<?php
// Daniel T. Gorski  dtg/240900/18:49/01
// Achtung: die Definition der $user- und $passwort-Variablen
// _sollte_ in einer externen Datei außerhalb des Document-Root
// festgelegt werden. Diese Datei muss dann an dieser Stelle
// [mit include() oder require()] importiert werden.
// Mehr dazu in dieser dclp-FAQ unter: "Wie kann ich mein
// Datenbankpasswort gegen Spionage sichern?"

// Datendefinition für Datenbankverbindung.
$host     = "localhost";  // MySQL - Zielrechner.
                          // Normallerweise ist es "localhost", bzw.
                          // synonym "127.0.0.1", also der Rechner,
                          // auf dem auch _dieses_ Script läuft.
$user     = "deinLogin";         // Dein Userlogin.
$password = "deinPasswort";      // Dein Datenbankpasswort.

$database = "nameDerDatenbank";  // Gewünschte Datenbank
                                 // innerhalb von MySQL
$table    = "nameDerTabelle";    // Der Name der Datenbanktabelle

// Datendefinition für die Clientausgabe
$start = (isset($start)) ? abs((int)$start) : 0;
$limit = 10;                     // Datensätze pro Ausgabeseite

// Verbindung zu MySQL-Datenbank herstellen oder sterben.
@mysql_connect($host,$user,$password)
   or die("Abbruch: Verbindung zu '$host'"
         ." konnte nicht hergestellt werden.");

// Benötigte Datenbank auswählen oder sterben.
@mysql_select_db($database)
   or die("Abbruch: Datenbank '$database' konnte nicht"
         ." selektiert werden.<br><br>MySQL sagt: ".mysql_error());

// Feststellen der Anzahl der verfügbaren Datensätze.
$resultID = @mysql_query("SELECT COUNT(ID) FROM ".$table);
$total    = @mysql_result($resultID,0);

// Ggf. $start korrigieren (falls Parameter in
// der URL manipuliert wurde)
$start    = ($start >= $total) ? $total - $limit : $start;

// Datenbankabfrage ausführen.
$query    = "SELECT ID,INHALT FROM ".$table
           ." LIMIT ".$start.",".$limit;
$resultID = @mysql_query($query);

// Ergebnisse lesen und an den Client ausgeben
while ($data = mysql_fetch_array($resultID))
{
  echo $data["ID"].": ".$data["INHALT"]."<br>";
}

// Zurück- und Vorblättern
if ($start > 0)
{
  $newStart = ($start - $limit < 0) ? 0 : ($start-$limit);
  echo "<a href=".$_SERVER['PHP_SELF']."?start=".$newStart
      .">&lt;&lt; zurück</a>";
}

if ($start + $limit < $total)
{
  $newStart = $start + $limit;
  echo " <a href=".$_SERVER['PHP_SELF']."?start=".$newStart
      .">vor &gt;&gt;</a>";
}

// Die benutzte (nichtpersistente) Verbindung zu der MySQL-Datenbank,
// wird nach dem Script-Ende automatisch geschlossen.
// That's it.
?>

Antwort von Frank Wiegand

Arbeitet man mit dem DB-Abstraktionslayer aus dem PEAR, kann man komfortabel die Pakete DB_Pager und Pager_Sliding einsetzen.

17.11. Wozu ist auto_increment nützlich? Wie erfahre ich den Wert des letzten Inkrements?

Keywords: SQL | Datenbank | MySQL | ID | auto_increment | Nummer

Antwort von Daniel T. Gorski

Ganzzahlige Datenbankfelder in MySQL können mit dem Attribut auto_increment versehen werden. Wird über die betreffende Tabelle eine INSERT-Query ausgeführt, so wird automatisch der Wert des mit auto_increment gekennzeichneten Feldes um Eins erhöht (inkrementiert), ohne dass dieses in der INSERT-Query explizit angegeben werden muss bzw. darf. Dies ist bei "flachen" Tabellen ohne Relationen nützlich z.B. bei Gästebüchern.

Mit dem MySQL-Monitor erzeugtes Beispiel:

mysql> CREATE DATABASE foo;
mysql> USE foo;

mysql> CREATE TABLE bar (
    ->    ID int(10) unsigned NOT NULL auto_increment,
    ->    INHALT varchar(32) NOT NULL,
    ->    PRIMARY KEY (ID)
    -> );

mysql> DESCRIBE bar;
+--------+------------------+------+-----+---------+----------------+
| Field  | Type             | Null | Key | Default | Extra          |
+--------+------------------+------+-----+---------+----------------+
| ID     | int(10) unsigned |      | PRI | 0       | auto_increment |
| INHALT | varchar(32)      |      |     |         |                |
+--------+------------------+------+-----+---------+----------------+

mysql> INSERT bar SET INHALT='erster Datensatz';
mysql> INSERT bar SET INHALT='zweiter Datensatz';

mysql> SELECT * FROM bar;
+----+-------------------+
| ID | INHALT            |
+----+-------------------+
|  1 | erster Datensatz  |
|  2 | zweiter Datensatz |
+----+-------------------+
2 rows in set (0.00 sec)

Wie man sehen kann, wird das Feld "ID" automatisch erhöht. Logischerweise darf nur ein Feld mit dem auto_increment Attribut versehen werden. Zusätzlich muss dieses Feld als Index definiert werden - z.B. als Primär-Schlüssel (PRIMARY KEY).

Um den Wert des letzten Inkrements erfahren, stellt PHP die Funktion mysql_insert_id() zur Verfügung:

<?php
// Es wird davon ausgegangen, dass $host, $user und
// $passwort korrekt initialisiert sind

// Verbindung zu MySQL-Datenbank herstellen oder sterben.
$linkID = mysql_connect($host,$user,$password)
   or die("Abbruch: Verbindung zu Host '$host' konnte"
         ." nicht hergestellt werden.");

// Benötigte Datenbank auswählen oder sterben.
@mysql_select_db("foo")
   or die("Abbruch: Datenbank '$database' konnte nicht"
         ." selektiert werden.<br><br>MySQL sagt: ".mysql_error());

// INSERT ausführen
@mysql_query("INSERT bar SET INHALT='dritter Datensatz'");

//  In unserem Beispiel ergibt das beim erstmaligen Aufruf "3"
//  dann "4", dann "5" etc.
echo mysql_insert_id($linkID);
?>

Die Funktion mysql_insert_id() liefert nichts zurück, wenn vorher keine INSERT-Query ausgeführt wurde; sie liefert einen falschen Wert, wenn der Typ des auto-increment-Feldes als BIGINT definiert wird, für die meisten Anwendungen sollte aber der Typ INT UNSIGNED mehr als ausreichend sein.

17.12. Wie lege ich den Initialwert des auto_increment fest? Läuft dieser Wert über?

Antwort von Daniel T. Gorski

Üblicherweise benutzt man Werte ohne Vorzeichen (UNSIGNED) für den Typ des auto_increment-Feldes. Ab der MySQL Server-Version 3.23 ist es auch nicht mehr möglich, in diesen Feldern negative Zahlen zu führen.

Geeignete Datentypen für auto_increment wären:

  • TINYINT UNSIGNED - Wertebereich 0 bis 255.

  • SMALLINT UNSIGNED - Wertebereich 0 bis 65535.

  • MEDIUMINT UNSIGNED - Wertebereich 0 bis 16777215.

  • INT UNSIGNED - Wertebereich 0 bis 4294967295.

Für die meisten Anwendungen ist der Datentyp INT UNSIGNED mehr als ausreichend - immerhin ermöglicht dieser eine Adressierung von über vier Milliarden Datensätzen!

Der Initialwert des auto_increment-Feldes ist 1. Um diesen Wert zu ändern, muss man ihn explizit setzen:

mysql> DELETE FROM bar;

mysql> INSERT bar SET INHALT='erster Datensatz';

mysql> SELECT * FROM bar;
+----+------------------+
| ID | INHALT           |
+----+------------------+
|  1 | erster Datensatz |
+----+------------------+

mysql> INSERT bar SET ID='1000', INHALT='zweiter Datensatz';

mysql> SELECT * FROM bar;
+------+-------------------+
| ID   | INHALT            |
+------+-------------------+
|    1 | erster Datensatz  |
| 1000 | zweiter Datensatz |
+------+-------------------+

mysql> INSERT bar SET INHALT='dritter Datensatz';

mysql> SELECT * FROM bar;
+------+-------------------+
| ID   | INHALT            |
+------+-------------------+
|    1 | erster Datensatz  |
| 1000 | zweiter Datensatz |
| 1001 | dritter Datensatz |
+------+-------------------+

Der auto_increment-Wert läuft nicht über - d.h. er wird nicht wieder negativ (bzw. Null bei UNSIGNED), wenn er über seinen Wertebereich hinaus adressiert wird. Stattdessen wird MySQL einen Fehler melden:

mysql> INSERT bar SET ID='4294967295', INHALT='letzter Datensatz';

mysql> SELECT * FROM bar;
+------------+-------------------+
| ID         | INHALT            |
+------------+-------------------+
|          1 | erster Datensatz  |
|       1000 | zweiter Datensatz |
|       1001 | dritter Datensatz |
| 4294967295 | letzter Datensatz |
+------------+-------------------+

mysql> INSERT bar SET INHALT='geht noch einer rein?';
ERROR 1062: Duplicate entry '4294967295' for key 1

17.13. Wie realisiere ich eine Volltextsuche mit MySQL?

Antwort von Matthias P. Wuerfl

Um eine Volltextsuche für eine Website zu realisieren, eignen sich speziell dafür erstellte Tools besser. Siehe hierzu auch Wie kann ich eine Volltextsuche realisieren?.

Liegen die Inhalte der Website in einer MySQL-Tabelle, so kann man jedoch auch MySQL zur Suche verwenden. Für den "Hausgebrauch" sollte das auf wenig belasteten Servern oft reichen. MySQL bietet hierzu ab der Version 3.23.23 die Möglichkeit, einen Volltextindex anzulegen.

Um die Spalte einer Tabelle mit einem solchen Index zu belegen muss das SQL-Statement ALTER TABLE tabellenname ADD FULLTEXT (textpalte) ausgeführt werden, welches einen entsprechenden Wortindex anlegt. Anschließend kann mit einer Query wie SELECT * FROM tabellenname WHERE MATCH textspalte1 AGAINST ('suchtext') der Index durchsucht werden. Dieser Wortindex reagiert nur auf ganze Worte, es kann also nicht nach Teilworten oder Wortkombinationen gesucht werden. Die Suche nach "Bauer" findet also nicht "Bauernhof".

Der Ausdruck MATCH a AGAINST b gibt einen Zahlenwert zurück, der die Relevanz des gefundenen Datensatzes wiedergibt, er kann also auch im SELECT-Teil eines SQL-Statements sinnvoll eingesetzt werden. Im ORDER BY-Teil des Statements braucht er nicht vorzukommen, denn MySQL sortiert automatisch nach Relevanz, wenn im WHERE-Teil der Volltextindex abgefragt wird.

SELECT   * FROM tabellenname
WHERE    MATCH textspalte AGAINST ('wort1 wort2')

...gibt alle Datensätze aus, in denen eines der Suchworte in textspalte vorkommt - nach Relevanz absteigend sortiert.

Hat man eine MySQL-Version älter als 3.23.23, dann kann man auch eine Volltextsuche realisieren. Diese geht jedoch dann wesentlich langsamer vonstatten und belastet den Datenbankserver unverhältnismäßig stark, da MySQL hier nicht den Index benutzen kann.

SELECT   * FROM tabellenname
WHERE    textspalte LIKE '%wort1%'
OR       textspalte LIKE '%wort2%'

Das Prozentzeichen hat im LIKE-Statement von SQL die Funktion, die man in anderen Situationen auch vom Sternchen (*) her kennt. Diese Query findet auch Teilwörter. Die Suche nach "Bauer" findet also auch "Bauernhof".

Antwort von Guido Haeger

Ab Version 4.0.1 bietet MySQL für den Ausdruck MATCH a AGAINST b zusätzlich einen BOOLEAN MODE. Dadurch ist es möglich genauere Suchabfragen zu definieren und die Gewichtung einzelner Spalten beim Ranking der Treffer zu beeinflussen

SELECT felder FROM tabellenname
WHERE MATCH textspalte AGAINST 
('+Docu -PHP' IN BOOLEAN MODE)

Eine Übersicht der zulässigen Operatoren und deren Wirkung findet sich in der MySQL-Dokumentation

17.14. Meine Datenbankabfrage/Mein SQL-Statement funktioniert nicht

Keywords: SQL | Datenbank | MySQL | Fehler | Error

Antwort von Clemens Koppensteiner

Folgende Abschnitte der FAQ könnten für Dich interessant sein:

17.15. Wie kann ich Umlaute richtig sortieren?

Keywords: SQL | Datenbank | MySQL | Sortieren | Umlaute

Antwort von Sven Eichler

Gelegentlich steht man vor dem Problem, Tabelleinhalt mit Text richtig sortiert auslesen zu müssen, welcher auch Umlaute enthält. Das ist aber bei MySQL nicht so ohne weiteres möglich, da MySQL standardmäßig einen Zeichensatz unterstützt, der eine Sortierung mit Umlauten nicht nach unserem natürlichen Verständnis unterstützt. Eine bessere Beschreibung dieses Problems findet sich in der MySQL-FAQ der Newsgroup de.comp.datenbanken.mysql.

Es gibt 3 Lösungsmöglichkeiten:

  • Wer Zugriff auf mysql-Einstellungen hat kann diese Möglichkeit anwenden: MySQL-FAQ

  • Erzeugen einer zusätzlichen Sortier-Spalte in MySQL-Tabellen

  • Direkt beim Auslesen der Daten eine Umlautformatierung und Sortierung vornehmen

zusätzlichen Sortier-Spalte

Anhand einer Tabelle mit Namen sei die Lösung mit der zusätzlichen Sortier-Spalte hier kurz erläutert. Follgende Tabelle ist gegeben:

+----+------------+
| ID |    Name    |
+----+------------+
|  1 | Äußerung   |
|  2 | Österreich |
|  3 | Überfluss  |
+----+------------+

Diese Tabelle wird nun um eine Spalte erweitert, in der später die Namen stehen, nach denen sortiert werden kann. Ich nenne die Spalte einfach sortiert. Diese Spalte sollte die gleichen Eigenschaften haben, wie die Spalte Name.

ALTER TABLE tabelle ADD sortiert VARCHAR(255) NOT NULL

Nun sieht unsere Tabelle so aus:

+----+------------+----------+
| ID |    Name    | sortiert |
+----+------------+----------+
|  1 | Äußerung   |          |
|  2 | Österreich |          |
|  3 | Überfluss  |          |
+----+------------+----------+

Die Umwandlung der Umlaute kann nun mit einer einzigen MySQL-Anweisung erfolgen:

mysql_query("UPDATE tabelle
              SET sortiert = REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE(
                             REPLACE(Name, 'Ä', 'A'), 'Ö', 'O'), 'Ü', 'U'),
                             'ä', 'a'), 'ö', 'o'), 'ü','u'), 'ß', 's')");

... Hierbei ist Name die entsprechende Tabellenspalte, aus der die umzuwandelnden Namen genommen werden. Es dürfen für den Spaltennamen keine Hochkommas verwendet werden!

Die fertige Tabelle sollte jetzt so aussehen:

+----+------------+------------+
| ID |    Name    |  sortiert  |
+----+------------+------------+
|  1 | Äußerung   | Ausserung  |
|  2 | Österreich | Osterreich |
|  3 | Überfluss  | Uberfluss  |
+----+------------+------------+

Richtig sortiert lässt sich das Ganze nun so auslesen:

$result = mysql_query("SELECT * FROM tabelle ORDER BY sortiert");

Anwendung in laufenden Scripten

mit MySQL:

$name = "irgend ein Name";
mysql_query("INSERT INTO tabelle (Name, sortiert)
             VALUES('$name', REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE(
                             REPLACE(Name, 'Ä', 'A'), 'Ö', 'O'), 'Ü', 'U'),
                             'ä', 'a'), 'ö', 'o'), 'ü','u'), 'ß', 's'))");

oder mit PHP:

$name = "irgend ein Name";
$sortiert = strtr($name, "ÄÖÜäöüß", "AOUaous");
mysql_query("INSERT INTO tabelle (Name, sortiert) VALUES('$name','$sortiert')");

Umlautsortierung wärend des Auslesens

Das Sortieren nach einer bestimmten Spalte kann auch während einer Abfrage stattfinden:

mysql_query("SELECT *, REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE(
                       REPLACE(Name, 'Ä', 'A'), 'Ö', 'O'), 'Ü', 'U'),
                       'ä', 'a'), 'ö', 'o'), 'ü','u'), 'ß', 's') AS sortiert
             FROM tabelle ORDER BY sortiert");

Zu bedenken ist dabei allerdings, dass die Serverlast eine nicht zu unterschätzende Rolle spielen kann. Wenn es zuviele gleichzeitige Zugriffe auf den Server gibt bzw. wenn es zuviele zu bearbeitende Datensätze gibt, kann die Performance empfindlich darunter leiden.

17.16. Wie kann ich mehrere Verbindungen zu MySQL öffnen?

Antwort von Johannes Frömter

Manchmal ist es sinnvoll, gleichzeitig mehr als eine Verbindung zum Datenbankserver offen zu haben (z.B. wenn man mit mehr als einer Datenbank arbeitet). Der Befehl mysql_connect() hat leider den Mangel, dass er bei wiederholtem Aufruf mit identischem Hostnamen, Benutzernamen und Kennwort immer dieselbe Verbindungskennung liefert. Anstatt für diesen Zweck verschiedene Benutzer anzulegen, kann man ihn jedoch auch mit unterschiedlichen Hostnamen austricksen - einmal mit localhost und einmal mit 127.0.0.1 aufgerufen, liefert er bereits zwei unterschiedliche Links...

Ab PHP Version 4.2 besitzt mysql_connect() einen optionalen Parameter, mit dem man eine neue Verbindung erzwingen kann.

17.17. Wie kann ich feststellen wie viele Datensätze von meiner Abfrage betroffen sind / gefunden wurden?

Keywords: SQL | Datenbank | MySQL | Datensatz | Zeile | Anzahl | Abfrage

Antwort von Guido Haeger

Mit den Funktionen mysql_num_rows() bzw. mysql_affected_rows() kann man die Anzahl der gefundenen Datensätze bei einem SELECT-Statement bzw. die Anzahl der betroffenen Datensätze bei einem UPDATE-/INSERT-Statement überprüfen.

// Beispiel für ein Select-Statement
$query = "SELECT * FROM table WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
    echo "Fehler: " . mysql_error();
}
else
{
    echo mysql_num_rows() . " Datensätze gefunden.<br />\n";
}

// Beispiel für ein UPDATE-Statement
$query = "UPDATE table SET a = '$a' WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
    echo "Fehler: " . mysql_error();
}
else
{
    echo mysql_affected_rows() . " Datensätze geändert.<br />\n";
}

18. Datenbanken: Oracle

18.1. Ora oder OCI?

Keywords: Oracle | Datenbank | Ora | OCI | Schnittstelle | LOB

Antwort von Thomas Fromm

Wenn eine Oracle Version ab 8.0.4 zur Verfügung steht sollte die OCI Schnittstelle verwendet werden. Diese unterstützt z.B. LOBs (Large Objects) und wird zudem noch weiterentwickelt.

18.2. Ich habe Oracle-Support mit --with-oci8 in PHP eincompiliert, nun startet der Apache nicht mehr.

Antwort von Anton Bangratz

Die häufigste Ursache ist:

Die shared libraries für den Oracle-Support werden nicht gefunden.

Lösung: Das Verzeichnis $ORACLE_HOME/lib in /etc/ld.so.conf eintragen und ldconfig aufrufen.

Eine weitere Möglichkeit wäre ein Fehler in der glibc-2.1. Behoben kann dies werden, indem den Apache neu gelinkt wird und dabei die Option -lpthread zu den LDFLAGS hinzugefügt wird. Dieser Fehler wurde in der glibc-2.2 behoben.

linux:/usr/src/apache_1_3_14/ # CFLAGS='-lpthread' ./configure ...

18.3. Der Webserver verbraucht jetzt viel mehr Speicher als ohne Oracle, mache ich was falsch?

Keywords: Oracle | Server | Speicher | Library

Antwort von Thomas Fromm

Nichts. Durch das Einbinden der Oracle Libs verbraucht der Webserver in der Regel erheblich mehr Speicher. Beim Apachen kann das schonmal auf 20 MB je Child anwachsen im Betrieb.

18.4. Umlaute, die in die Datenbank eingetragen wurden, werden nicht korrekt dargestellt.

Antwort von Anton Bangratz

Sowohl beim Eintragen als auch beim Auslesen der Daten in Textfeldern ist darauf zu achten, dass der verwendete Client die richtigen NLS/-Parameter verwendet. Die Clients bekommen diese Information über die Umgebungsvariablen NLS_LANG und ORA_NLS33. Der richtige Wert für NLS_LANG ist der Datenbank selber zu entnehmen. Hat die Datenbank zum Beispiel als Character Set die Einstellung WE8ISO8859P9, so sollte die Variable {SPRACHE}_{LAND}.WE8ISO8859P9 lauten, wobei {SPRACHE} und {LAND} nur für die Steuerung der Meldungen, die der Client zurückgibt, zuständig sind, und vom mit dem ORACLE-Client installierten Sprachpaket abhängig sind.

Die Einstellung von ORA_NLS33 dient dazu, dem Client mitzuteilen, wo sich die Dateien befinden, die die Prompts in verschiedenen Sprachen beinhalten.

Ein Beispiel für korrekte Einstellung:

export ORACLE_HOME=/opt/oracle/OraHome1
export ORA_NLS33=$ORACLE_HOME/ocommon/nls/admin/data
export NLS_LANG GERMAN_GERMANY.WE8ISO8859P9

Nun ist es noch wichtig, dass die Variablen von PHP korrekt initialisiert werden. Dabei ist darauf zu achten, dass man beim Modul-PHP diese Variablen vor dem Start des Apache setzt - im Script apachectl ist beispielsweise ein guter Platz dafür.

18.5. Gibt es auto_increment unter Oracle?

Antwort von Thomas Fromm

Nein. Jedoch kann auto_increment via Trigger emuliert werden.

Beispiel:

Wer von MySQL nach Oracle portiert vermisst wohl als erstes das auto_increment Feature von MySQL Spalten. Dieses läßt sich jedoch unter der Nutzung von Sequences und Triggern bei Oracle erstellen:

rem Wir brauchen zum einen einen Zähler,
rem der hochzählt (dazu die Sequence)

create sequence zaehler_der_tabelle_xy
       increment by 1 start with 1 cache 2;

rem Jetzt die eigentliche Tabelle

CREATE TABLE xy (
        id_xy NUMBER(20,0) PRIMARY KEY,
        bla_xy VARCHAR2(4000)
);

rem Nun ist noch ein Trigger vonnöten, der
rem die neue Id von der Sequence übergeben
rem bekommt und vor dem insert diesen Wert
rem auf die entsprechende Spalte überträgt:

CREATE TRIGGER trigger_primary_key BEFORE INSERT ON xy
       REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
Begin
select zaehler_der_tabelle_xy.nextval into :NEW.id_xy from DUAL;
End;
/

Bei jedem insert wird nun der Wert von id_xy automatisch hochgesetzt. Zu beachten ist, dass es besser ist, für jede Tabelle eine eigene Sequence zu erstellen, denn sonst müssen sich alle Tabellen die fortlaufenden Zahlen teilen.

Alternativ kann auch ohne Trigger gearbeitet werden und die nächste Zahl per Hand selektiert werden:

$stmt=OCIParse($conn, "SELECT zaehler_der_tabelle_xy.nextval FROM DUAL");
OCIExecute($stmt);
OCIFetch($stmt);
$nextid=OCIResult($stmt, "NEXTVAL");

18.6. Ich verwende das obige Beispiel. Wie kann ich nun mysql_insert_id() emulieren?

Antwort von Thomas Fromm

Der aktuellen Wert der Sequenz zaehler_der_tabelle_xy kann durch Verwendung von currval (abgeleitet von Current Value) ermittelt werden. Mit dem folgenden Codestück ist es möglich, den zuletzt eingefügten Wert abzufragen:

// INSERT in die Tabelle

$stmt = OCIParse($conn, "INSERT INTO xy (BLA_XY) VALUES 'BLA'");
OCIExecute($stmt, OCI_DEFAULT);

//Abfrage der Sequence
$stmt = OCIParse($conn,"SELECT zaehler_der_tabelle_xy.currval
                 AS CV FROM DUAL");
OCIExecute($stmt, OCI_DEFAULT);
OCIFetch($stmt);
$last_id=OCIResult($stmt, "CV");
OCICommit($conn);

Achtung: Das selektieren des currval funktioniert nur innerhalb derselben Transaktion (daher auch beim OCI_DEFAULT). Will man den aktuellen höchsten ID-Wert ermitteln, ist es besser, unter Verwendung der SQL-Funktion MAX() den höchsten Wert direkt aus der Tabelle abzufragen.

18.7. Wie selektiere ich nur bestimmte Zeilen (LIMIT unter MySQL)?

Antwort von Thomas Fromm

Da Oracle über kein Limit verfügt gestaltet sich die Abfrage etwas komplizierter (das Beispiel funktioniert erst ab Version 8.1):

SELECT *
  FROM
   (SELECT ROWNUM rownum2, inline_view1.*
      FROM
       (SELECT ROWNUM rownum1, ename, hiredate
          FROM emp
          ORDER BY hiredate
       ) inline_view1  -- zum Sortieren (ROWNUM hier noch ungeordnet)
   ) inline_view2      -- ROWNUM spiegelt jetzt die Sortierung wider
WHERE rownum2 BETWEEN 5 AND 7

Dies ist nicht sonderlich schnell, weil die innere Abfrage alle Zeilen auswählt. Für Versionen ab 8.1.6 geht auch folgendes:

SELECT *
  FROM
   (SELECT ROW_NUMBER()
    OVER(ORDER BY hiredate) rownum1, ename, hiredate
      FROM emp
   ) inline_view1
WHERE rownum1 BETWEEN 5 AND 7
/

Will man lediglich n Zeilen ausgeben tuts auch dies:

SELECT * FROM
  (SELECT ename, hiredate FROM emp ORDER BY hiredate)
WHERE ROWNUM < 6

18.8. Wie speichere ich Datensätze mit mehr als 2000 Zeichen ab?

Antwort von Thomas Fromm

Um diese Datenfelder abzuspeichern, muss zuerst Speicher angefordert werden, dies geschieht mit OCIBindByName() .

$req="INSERT INTO wurstbrote (name) VALUES (:name)";
$stmt=OCIParse($conn, $req);
// nun binde ich den Inhalt von $wurstbrotname
// an den Oracle Platzhalter :name
OCIBindByName($stmt,":name",$wurstbrotname,-1);
OCIExecute($stmt);

Trotz dieser Umständlichkeit gestaltet sich das Lesen/Schreiben von grösseren Datensätzen performanter als z.B. bei MySQL.

18.9. Wie bearbeite ich LOBs mit PHP?

Keywords: Oracle | Datenbank | LOB | speichern

Antwort von Thomas Fromm

Diese Codebeispiele beschreiben insert, update und select:

// INSERT:
$req = "INSERT INTO bdata (description, data) VALUES
        (:description, EMPTY_BLOB()) returning data into :data";
$stmt = OCIParse($conn, $req);
$lob = OCINewDescriptor($conn, OCI_D_LOB);
OCIBindByName($stmt, ":description", $description, -1);
OCIBindByName($stmt, ":data", $lob, -1, OCI_B_BLOB);
OCIExecute($stmt, OCI_DEFAULT);
if($lob->save($bdata)) {
 OCICommit($conn);
} else {
 echo "Problems: Couldn't upload Lob\n";
}
OCIFreeDesc($lob);
OCIFreeStatement($stmt);

// Für die verwendeten Typen beim OCIBindByName auf jeden Fall mal
// ins PHP Handbuch schauen.

// UPDATE:
// Das SELECT FOR UPDATE sperrt den Eintrag für
// andere Schreibzugriffe
// Diese Sperre wird erst beim nächsten Commit aufgehoben
$req="SELECT data FROM bdata WHERE id='5' FOR UPDATE";
$stmt=OCIParse($conn, $req);
OCIExecute($stmt, OCI_DEFAULT);

$req="UPDATE bdata SET data=:data WHERE id='5'";
$stmt=OCIParse($conn, $req);
OCIBindByName($stmt, ":data", $data, -1);
OCIExecute($stmt, OCI_DEFAULT);
OCICommit($conn);

// SELECT
$req="SELECT data FROM bdata WHERE id='5'";
$stmt=OCIParse($conn, $req);
OCIExecute($stmt);
OCIFetch($stmt);
$bdatalob=OCIResult($stmt, "DATA");
$bdata=$bdatalob->load();

18.10. Wie nenne ich Spalten um?

Antwort von Thomas Fromm

Ab Version 9.2.0 bietet Oracle folgende einfache Möglichkeit, eine Spalte umzubenennen:

alter table tabellenname rename column alter_name to neuer_name;

Hierfür sind keine DBA Rechte notwendig.

Wenn eine ältere Version von Oracle eingesetzt wird, ist "alter table rename column" nicht vorhanden und sofern man DBA Rechte hat kann man dies über einen Midnighthack lösen:

(Ist mit Vorsicht zu geniessen und am besten nicht zu benutzen :-)

update SYS.COL$ col set col.NAME = 'neuer_name'
where col.NAME = 'alter_name' and col.OBJ# in (
    select ob.OBJ# from SYS.OBJ$ ob, SYS.USER$ us
    where ob.OWNER# = us.USER# and us.NAME = 'besitzername'
    and ob.NAME = 'alter_name'
);

Besitzername ist der Name des Besitzers der Tabelle.

Wichtig: Alle Namen müssen in Grossbuchstaben angegeben werden!

18.11. Wie kann ich SQL Skriptdateien in Oracle ausführen?

Keywords: Oracle | Datenbank | SQL | Skript

Antwort von Thomas Fromm

Einfach SQL*Plus starten und dann:

SQL> @meinedatei.sql

18.12. Welche freien Tools gibts für Oracle?

Antwort von Thomas Fromm

Zu empfehlen ist der Oracle Objectmanager von OraSoft. Seit kurzem gibt es auch ein ähnliches in PHP geschriebenes Tool phpOracleAdmin. Wer TOAD von Windows gewohnt ist, wird sich auf TOra freuen, dies ist eine freie, ja man könnte sagen, PL/SQL IDE.

18.13. Ich bekomme ein Oracle Fehlernummer ORA-XXXXX, wo stehen die Fehlercodes?

Antwort von Thomas Fromm

Oracle Fehlermeldungen bestehen aus einem Fehlerbereich (ORA, OCI ...) und einer 5-stelligen Fehlernummer. Die komplette Fehlerbeschreibung bekommt man mit:

linux:# / oerr ora <nummer>

Vorrausgesetzt die Pfade ins $ORACLE_HOME/bin sind gesetzt, erscheint der volle Fehlertext. Kommt allerdings eine Fehlermeldung der Art:

Cannot find /u01/8.1.6/rdbms/mesg/orad.msg file.

ist die Oracle Installation eine teilweise ans deutsche angepasste Version. (oraus.msg ist die original Datei und orad.msg ist eine deutsche Version) Da die orad.msg nciht in allen Fällen vorhanden ist, empfiehlt es sich einen symbolischen Link zu setzen.

linux:# / cd /u01/8.1.6/rdbms/mesg/
linux:# / ln -s oraus.msg orad.msg

Dannach sollte man zumindest die englischen Fehlertexte erhalten.

18.14. Welche Bücher zu Oracle sind empfehlenswert?

Keywords: Oracle | Datenbank | Buch | lernen

Antwort von Thomas Fromm

Für das nötige Basiswissen empfiehlt sich die "Oracle Referenz", die auch Einsteigern u.a. den Zugang zu PL/SQL erleichtert.

"Oracle8 für den DBA" trägt zum alltäglichen Umgang mit Oracle bei. Gerade im produktiven Einsatz, zeigt sich, das die administrative Seite der Datenbank nicht zu unterschätzen ist.

Gerade bei Webapplikationen ist Performance und Reaktionszeit wichtig, ich empfehle hier "Oracle 8. Tuning.".

Während die Oracle Referenz ins Regal eines jeden Entwicklers gehört, der auf dieser Datenbank Applikationen entwickelt, bieten die beiden anderen Bücher eine Abrundung der Nachschlagewerke für den allgemeinen Umgang. Die dort aufgeführten Beispiele sind verständlich geschrieben und leicht nachzuvollziehen. (Ein bisschen gewöhnungsbedürftig ist allerdings bei Oracle Press Bücherübersetzungen die Indizierung...)

Wenn man mehr auf Optimierung und Performance ausgerichtet ist, dem kann ich nur "Oracle PL/SQL Programmierung" ans Herz legen. Neben einer ausführlichen Behandlung von PL/SQL werden dort auch wichtige Packages wie z.B. DBMS_JOB, der Oracle interne Cronjobmechanismus erklärt. Zusätzlich gibt es auch ein Kapitel, welches die Einbindung von Externen Prozeduren (welche man in C oder Java Programmieren kann) erläutert anhand von Beispielen.

19. Datenbanken: Sybase

19.1. Sybase-DB oder Sybase-CT?

Keywords: Sybase | Datenbank | DB-lib | CT-lib

Antwort von Timm Friebe

Der Unterschied wird in der Sybase FAQ erklärt: 7.2: What is the difference between DB-lib and CT-lib? (Englisch).

Am besten also PHP --with-sybase-ct[=/pfad/zu/sybase/libraries] compilieren.

19.2. FreeTDS

Antwort von Timm Friebe

Für alle, die nicht in den Genuss der Sybase-Client-Libraries kommen, gibt es FreeTDS - das ist eine freie Implementation des der Sybase zugrundeliegenden TDS- ("Tabular Data Streams") Protokolls. FreeTDS ist in der Version 0.62 relativ stabil (und auf jeden Fall ausreichend stabil für normale PHP-Anwendungen). Einige Bugs zeigen sich beim Canceln von Resultsets auf, die sich je nach Lage entweder in Segmentation Faults oder unendlich vielen Meldungen "Unknown Marker: XXX" äußern. PHP "cancelt" alle Resultsets bis auf das erste - multiple Resultsets werden beispielsweise von der Stored Procedure sp_help zurückgegeben.

Für die freetds.conf ist die TDS-Version 5.0 zu wählen. Beispiel:

[gurke]
        host = 127.0.0.1
        port = 1999
        tds version = 5.0

19.3. String-Quoting bei Sybase

Antwort von Timm Friebe

Bei Sybase müssen Anführungsstriche Strings innerhalb von Queries nicht mit einem Backslash (\), sondern mit dem jeweils gleichen Zeichen escaped werden. Folgendes Beispiel sollte das anschaulich machen:

select "Er sagte: ""Hallo Welt"""
select 'Das gibt''s doch nicht'

Eine Komfort-Funktion, die sich darum kümmert, dass in SQL-Statements immer richtig gequotet wird, soll hier gezeigt werden:

function sybase_prepare() {
  $args= func_get_args();
  $sql= $args[0];
  if (sizeof($args)<= 1) return $sql;
  $j= 0;    
  $sql= $tok= strtok($sql, '%');
  while (++$j && $tok= strtok('%')) {
    $arg= (is_object($args[$j]) && method_exists($args[$j], 'toString') 
      ? $args[$j]->toString()
      : $args[$j]
    );
    switch ($tok{0}) {
      case 'd': 
        $sql.= ($arg === NULL ? 'NULL' : intval($arg)).substr($tok, 1); 
        break;
        
      case 'c': 
        $sql.= substr($tok, 1); 
        break;
        
      case 's': 
        $sql.= ($arg === NULL ? 'NULL' : "'".str_replace("'", "''", $arg)."'").substr($tok, 1); 
        break;
        
      default: 
        $sql.= '%'.$tok; $j--;
    }
  }
  return $sql;
}

// Beispiel 1
$sql= sybase_prepare('
  insert into person (
    person_id, name, company
  ) values (
    %d, %s, %s
  )',
  1,
  'Dau Jones',
  NULL
);

// Beispiel 2
class Date {
  var $utime;
  
  function Date($utime) {
    $this->utime= $utime;
  }
  
  function toString($fmt= 'Y-m-d H:i:s') {
    return date($fmt, $this->utime);
  }
}

$date= &new Date(time());
$sql= sybase_prepare(
  'select count(*) from %c where lastchange= %s',
  'account',
  $date
);

Antwort von Kerry W. Lothrop

Durch das Einschalten des Parameters magic_quotes_sybase ändert sich auch das Verhalten der Funktion addslashes() , selbst wenn die Parameter magic_quotes_gpc und magic_quotes_runtime deaktiviert sind.

$string = '\' \ "';

// magic_quotes_sybase = Off
echo addslashes($string);
Ergibt \' \\ \"

// magic_quotes_sybase = On
echo addslashes($string);
Ergibt '' \ "

19.4. MySQL-Kompatibilität: Tabellen auflisten

Antwort von Timm Friebe

Bei MySQL gibt es die Funktion mysql_list_tables() , um Tabellen innerhalb der aktuellen Datenbank aufzulisten. Bei Sybase gibt es die Funktion nicht, hier muss auf den folgenden Select zurückgegriffen werden:

select name from sysobjects where type= 'U'

19.5. MySQL-Kompatibilität: Datenbanken auflisten

Antwort von Timm Friebe

Analog dazu wird eine Liste der Datenbanken abgefragt. Während MySQL die Funktion mysql_list_dbs() kennt, muss bei Sybase folgender Select verwendet werden:

select name from master..sysdatabases

19.6. MySQL-Kompatibilität: Server-Info

Antwort von Timm Friebe

Die Funktion mysql_get_server_info() kann wie folgt emuliert werden:

select @@version

Ein Ausgabe-Beispiel ist SQL Server/11.0.3.3 ESD#6/P-FREE/Linux Intel/Linux 2.2.14 i686/1/OPT/Fri Mar 17 15:45:30 CET 2000

19.7. MySQL-Kompatibilität: Prozesse auflisten

Antwort von Timm Friebe

Eine Liste der aktiven Prozesse (wie mysql_list_processes() ) kann wie folgt abgerufen werden:

select 
  p.spid, 
  p.hostname, 
  p.cmd, 
  d.name 
from 
  master..sysprocesses p, 
  master..sysdatabases d 
where 
  d.dbid= p.dbid

19.8. Sybase: Changed database context...

Antwort von Timm Friebe

Die Warning Sybase: Changed database context... kann entweder durch das Voranstellen des Zeichens @ vor die Funktion sybase_select_db() oder aber dauerhaft durch das Ändern des INI-Eintrags sybct.min_server_severity auf den Wert 11 (per Default ist dieser 10) erreicht werden.

19.9. Hostnamen definieren

Antwort von Timm Friebe

Der Hostname (wie in sp_who zu sehen) ist per Default das relativ unaussagekräftige "PHP 4.0". Dieser Wert kann über den INI-Eintrag sybct.hostname auf einen beliebigen Wert geändert werden (bspw. "oltp" oder "cronjob").

19.10. Textfelder

Keywords: Sybase | Datenbank | Textfelder

Antwort von Timm Friebe

In Sybase gibt es Datenfelder vom Typ text, die bis zu 2 GB Daten enthalten können. Bei einem Select mit PHP werden jedoch nur 32 KiloByte gelesen. Sollen mehr Daten gelesen werden können, muss vorher folgender Query abgesetzt werden:

set textsize <<max_bytes>>

Sollen maximal 256 Kilobyte gelesen werden können, sähe das also so aus:

set textsize 262144

20. Datenbanken: Microsoft SQL Server

20.1. SQL-Benutzer kann sich nicht anmelden

Antwort von Frank Staude

Wenn man den SQL-Server 2000 installiert und dabei die Option "Standard" wählt, wird die Authentifizierung in dem Modus "Nur Windows" eingerichtet. Das heisst, dass man zwar im SQL-Server Benutzer einrichten kann, diese sich aber nicht anmelden können (z.B: mit dem QueryAnalyser). Als Fehlermeldung kommt:

Server: Nacht-Nr. 18452, Schweregrad 16, Status 1 
Fehler bei der Anmeldung für den Benutzer "Benutzername". 
Ursache: Keiner vertrauten SQL-Server-Verbindung zugeordnet. 

Um den SQL-Server davon zu überzeugen, dass auch Benutzer des SQl-Servers darauf zugreifen dürfen und nicht NUR Windows-User, kann man bei der Installation den Modus "Benutzerdefiniert" wählen - dann wird der Authentifizierungsmodus erfragt. Bei einem bereits installierten SQL-Server kann dies nachträglich geändert werden, indem man den Server im EnterpriseManager auswählt (Konsolenstamm/Microsoft SQL-Servers/SQL-Server Gruppe/Name des Servers). Von dem zu ändernden SQL-Server wählt man das Kontextmenü an und ruft den Punkt "Eigenschaften" auf. Dort dann das Feld "Sicherheit" auswählen und Authentisierung auf "SQL-Server und Windows" umstellen.

20.2. Kein mssql_affected_rows in PHP vorhanden

Keywords: SQL | Datenbank | MSSQL

Antwort von Frank Staude

Bei MySQL liefert die Funktion mysql_affected_rows() die Anzahl betroffener Datensätze einer vorhergehenden MySQL Operation zurück. Diese Funktion für den SQL-Server, mssql_affected_rows ist nicht vorhanden.

Die PHP ab Version 4.0.4 heisst die Funktion mssql_rows_affected() , statt wie erwartet mssql_affected_rows. In PHP vor Version 4.0.4 können Sie sich dadurch behelfen das Sie diesen Wert aus den Systemvariablen des SQL-Servers ermitteln Lesen Sie nach einer Datenbankoperation die Variable @@rowcount aus.

list($rows) = mssql_fetch_row(mssql_query('select @@rowcount', $dbh));

20.3. Nach Textfeldern sortieren

Keywords: SQL | Datenbank | MSSQL | Text | Sortorder

Antwort von Frank Staude

MSSQL kann kein sortorder auf Felder vom Typ Text. Um dennoch ein solches Feld als Sortierkriterium verwenden zu können, muss man dem MSSQL sagen das er nur eine bestimmte Anzahl an Zeichen berücksichtigen soll. Dies wird im Query mit dem SUBSTRING Kommando gemacht.

Um die ersten 40 Zeichen für die Sortierung zu berücksichtigen muss man folgendes abschicken.

select * from tabelle order by substring(spaltenname,1,40)

20.4. Sonderzeichen " und ' werden nicht korrekt escaped

Antwort von Frank Staude

Beim schreiben in die MSSQL Datenbank erscheinen " als \" und ' geben einen Fehler aus, trotz korrekter Anwendung Addslashes/Stripslashes.

MSSQL stammt von Sybase ab. Die Sonderzeichen müssen mit ' escaped werden und nicht mit \. Die Lösung, die überall funktioniert, ist eine eigene addslashes/stripslashes-Funktion zu schreiben. Die zweite Lösung erfordert zwar weniger Aufwand, benötigt jedoch Zugriff auf die Konfiguratinsdatei php.ini. Setzen Sie dort den Eintrag für die magic_quotes_sybase auf on. Aus Gründen der Portabilität ist von dieser Lösung aber dringend abzuraten.

magic_quotes_sybase = On

Diese Problematik und deren Lösungsansätze werden auch in String-Quoting bei Sybase näher beschrieben.

20.5. Mein Spaltenname ist länger als 32 Zeichen und mssql_fetch_array liefert einen leeren String

Antwort von Frank Staude

mssql_fetch_array schneidet die Spaltennamen, die dann als Key in dem Array verwendet werden, nach 32 Zeichen ab. Um trotzdem an die Daten zu kommen haben Sie zwei Möglichkeiten.

  1. Sie geben den betroffenen Spalten in ihrem Query mit as einen anderen Namen.

    select SpaltenNameMitMehrAls32Zeichen as KurzerName from Tabelle;
    
  2. Wenn das nicht so ohne weiteres möglich ist, z.b. weil die Spaltennamen erst zusammengebaut werden, dann können Sie beim Zugriff auf das Array den Namen bei 32 Zeichen abschneiden.

    $name = "SpaltenNameMitMehrAls32Zeichen";
    $rs = mysql_fetch_array();
    $wert = $rs[ substr( $name, 0, 31 ) ];
    

20.6. MySQL-Kompatibilität: Tabellen auflisten

Antwort von Frank Staude

Timm Friebe hat im Kapitel Datenbanken: Sybase für Sybase Datenbanken unter MySQL-Kompatibilität: Tabellen auflisten es ebenfalls beschrieben.

Bei MySQL gibt es die Funktion mysql_list_tables() , um Tabellen innerhalb der aktuellen Datenbank aufzulisten. Bei MSSQL gibt es die Funktion nicht. Um diese Funktion nachzubilden, lesen Sie die Spalte name aus der Tabelle sysobjects aus, allerdings nur die vom type U, das sind die User-tables. Andernfalls bekommen Sie alle Tabellen (inkl. der Systemtabellen) zurückgeliefert.

select name from sysobjects where type= 'U'

20.7. MySQL-Kompatibilität: Datenbanken auflisten

Keywords: SQL | Datenbank | MSSQL | mssql_list_dbs

Antwort von Frank Staude

Timm Friebe hat im Kapitel Datenbanken: Sybase für Sybase Datenbanken unter MySQL-Kompatibilität: Datenbanken auflisten es ebenfalls beschrieben.

Analog dazu wird eine Liste der Datenbanken abgefragt. Während MySQL die Funktion mysql_list_dbs() kennt, muss bei MSSQL folgender Select verwendet werden:

select name from master..sysdatabases

20.8. MySQL-Kompatibilität: Server-Info

Antwort von Frank Staude

Timm Friebe hat im Kapitel Datenbanken: Sybase für Sybase Datenbanken unter MySQL-Kompatibilität: Server-Info es ebenfalls beschrieben.

Die Funktion mysql_get_server_info() kann wie folgt emuliert werden:

select @@version

Ein Ausgabe-Beispiel ist Microsoft SQL Server 2000 - 8.00.194 (Intel X86) Aug 6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Developer Edition on Windows NT 5.0 (Build 2195: Service Pack 2)

20.9. MySQL-Kompatibilität: Prozesse auflisten

Antwort von Frank Staude

Timm Friebe hat im Kapitel Datenbanken: Sybase für Sybase Datenbanken unter MySQL-Kompatibilität: Prozesse auflisten es ebenfalls beschrieben.

Eine Liste der aktiven Prozesse (wie mysql_list_processes() ) kann wie folgt abgerufen werden:

select 
  p.spid, 
  p.hostname, 
  p.cmd, 
  d.name 
from 
  master..sysprocesses p, 
  master..sysdatabases d 
where 
  d.dbid= p.dbid

21. phpMyAdmin

21.1. Was ist phpMyAdmin?

Antwort von Tobias Ratschiller

phpMyAdmin ist eine in PHP geschriebene Verwaltungsoberfläche für MySQL. Weitere Informationen dazu finden Sie auf der Homepage.

21.2. Ich bin kein MySQL-Administrator. Wie kann ich phpMyAdmin nur für mich selbst installieren?

Antwort von Tobias Ratschiller

Holen Sie sich die Distribution (TarGz or Zip) von der phpMyAdmin-Homepage. Bitte folgen Sie dann den Anweisungen in der Datei INSTALL; für's erste Ausprobieren genügt es, in die Datei config.inc.php Ihren MySQL-Benutzernamen und -Passwort einzutragen.

21.3. Ich bin MySQL-Administrator und möchte ein Exemplar phpMyAdmin für alle meine User installieren.

Antwort von Tobias Ratschiller

Seit phpMyAdmin 2.0.3 ist es möglich, eine zentrale Kopie von phpMyAdmin zu installieren, in die sich die einzelnen Benutzer mit Benutzername und Passwort einloggen. phpMyAdmin benutzt dafür das Rechte-System von MySQL. Benutzer müssen daher korrekt in das Rechte-System eingetragen sein: Für jeden Benutzer, der auf phpMyAdmin zugreifen können soll, muss ein Eintrag in die mysql.user und mysql.db-Tabelle gemacht werden. Um dem Benutzer foo Zugriff auf die Datenbank foo_db zu geben, würden Sie folgende SQL-Statements benutzen:

INSERT INTO user (Host, User, Password, Select_priv, Insert_priv,
                 Update_priv, Delete_priv, Create_priv, Drop_priv,
                 Reload_priv, Shutdown_priv, Process_priv,
                 File_priv, Grant_priv, References_priv, Index_priv,
                 Alter_priv)
          VALUES ('localhost', 'foo', PASSWORD('bar'), 'N', 'N',
                 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N',
                 'N', 'N')
INSERT INTO db   (Host, Db, User, Select_priv, Insert_priv,
                 Update_priv, Delete_priv, Create_priv, Drop_priv,
                 Grant_priv, References_priv, Index_priv, Alter_priv)
          VALUES ('localhost', 'foo_db', 'foo', 'Y', 'Y', 'Y', 'Y',
                 'Y', 'Y', '', '', '', '')

Bitte beachten Sie, dass Sie nach dieser Änderung ein FLUSH PRIVILEGES-Statement ausführen müssen, damit sie wirksam wird.

Seit phpMyAdmin 2.0.6 werden auch Wildcards im Rechte-System unterstützt; damit können Sie dem Benutzer foo beispielsweise Zugriff auf alle Datenbanken geben, deren Name mit foo_ beginnt. Weitere Informationen zum Setup eines solchen Benutzers finden Sie im MySQL-Handbuch.

21.4. Wieso kann ich den Inhalt meiner Tabelle nicht editieren?

Antwort von Tobias Ratschiller

Bis phpMyAdmin 2.0.6 können Sie den Inhalt einer Tabelle nur dann ändern, wenn ein Primärschlüssel in der Tabelle gesetzt ist. Ab 2.0.6 können Sie den Inhalt in allen Fällen editieren. Falls kein Primärschlüssel existiert, wird allerdings der Inhalt aller Zeilen mit zu der aktuellen Zeile äquivalenten Inhalten geändert, da es im relationalen Datenbankmodell unmöglich ist, diese Zeilen voneinander zu unterscheiden.

Es ist gutes Datenbankdesign, wenn man für eine jede Tabelle eine Spalte (atomarer Primärschlüssel) oder eine Kombination von Spalten (zusammengesetzter Primärschlüssel) als Primärschlüssel deklariert, sodass keine zwei Zeilen existieren können, die in den als Primärschlüssel deklarierten Spalten dieselben Werte haben können. Alle Zeilen werden durch ihre Primärschlüsselwerte überhaupt erst unterscheidbar.

21.5. Wieso werden TIMESTAMP-Felder nicht auf die aktuelle Zeit gesetzt, wenn ich eine neue Zeile einfüge?

Keywords: MySQL | phpMyAdmin | timestamp | NULL | leer

Antwort von Tobias Ratschiller

phpMyAdmin trägt einen leeren String ('') ein, wenn Sie keinen Wert angeben. MySQL konvertiert dies zu 0000-00-00 00:00:00. Um den Default-Wert eines Feldes einzutragen (im Falle von TIMESTAMP die aktuelle Zeit) geben Sie im Eintragsformular "null" (ohne Anführungszeichen) als Wert ein.

21.6. Wieso kann ich in phpMyAdmin mehrere durch Semikolon getrennte SQL-Statements ausführen, nicht aber mit normalen PHP-Funktionen?

Keywords: MySQL | phpMyAdmin | SQL | Semikolon

Antwort von Tobias Ratschiller

SQL selbst definiert nur einzelne Anweisungen, keine Anweisungsfolgen. phpMyAdmin trennt die Zeichenkette auf und generiert dann automatisch mehrere einzelne Anfragen. PHP selbst macht das nicht, also müssen Sie selbst die Anweisungen nacheinander einzeln senden.

21.7. Fehler: Die zusätzlichen Funktionen für verknüpfte Tabellen wurden automatisch deaktiviert.

Antwort von Frank Wiegand

Seit Version 2.2.4 bietet phpMyAdmin die Möglichkeit, Relationen zwischen Tabellen zu definieren. Diese Relationen sind nur für den Gebrauch von phpMyAdmin, sie wirken sich nicht auf die Tabellen selbst aus. Dazu muss eine Datenbank definiert, sowie einige Tabellen angelegt werden. Kann phpMyAdmin diese Tabellen nicht finden, kommt es zu der Fehlermeldung. Wie man die Tabellen anlegt und phpMyAdmin entsprechend konfiguriert, steht in der Dokumentation.

21.8. "Das $cfg['PmaAbsoluteUri']-Verzeichnis MUSS in Ihrer Konfigurationsdatei angegeben werden!"

Antwort von Frank Wiegand

phpMyAdmin nutzt in einigen Dateien eine HTTP-Umleitung nach RFC 2616, 14.30 (HTTP/1.1). Das RFC verlangt, dass der Location-Header immer einen absoluten URI sendet. In die Konfigurationsdatei config.inc.php(3) trägt man also ein:

$cfg['PmaAbsoluteUri'] = 'http://example.com/phpmyadmin/';

Wobei http://example.com/phpmyadmin/ die Adresse ist, unter der man phpMyAdmin im Browser erreichen kann. Der Slash am Ende darf nicht vergessen werden. Seit Version 2.3.0 versucht phpMyAdmin diesen Wert selbständig zu ermitteln. Die Meldung verschwindet dadurch allerdings nicht. Man kann sie abschalten, indem man in der Konfigurationsdatei folgendes einträgt:

$cfg['PmaAbsoluteUri_DisableWarning'] = FALSE;

Mehr Informationen dazu findet man in der Dokumentation.

22. Grafikfunktionen

22.1. Wie kann ich Thumbnails von einer Webseite erzeugen lassen?

Keywords: Grafik | Thumbnail | Webseite | URL | Vorschau

Antwort von Kristian Köhntopp

KDE bietet das Kommandozeilenprogramm kwebdesktop an, das vom KDE HTML Widget Gebrauch macht. Dieses Programm lädt eine Webseite und wandelt sie in ein PNG-Bild um. Es wird folgendermaßen aufgerufen:

kwebdesktop x y datei url

x ist die Breite des Bildes in Pixeln, y die Höhe des Bildes in Pixeln, datei der Name der zu erzeugenden PNG-Datei und url die URL der zu ladenden Seite.

In Suse Linux ist kwebdesktop Bestandteil des Paketes kdebase, getestet wurde Version 2.2.1.

22.2. Wie kann ich mit PHP Diagramme erstellen?

Antwort von Johannes Frömter

Es gibt mehrere fertige Scripte, die die Erzeugung von Diagrammen übernehmen. Eine Auflistung findet sich hier: Welche Lösungen gibt es, um Diagramme zu erstellen?

22.3. Wie kann ich Bilder verkleinern?

Antwort von Johannes Frömter

Mit den Image-Funktionen von PHP lassen sich Grafiken direkt mit PHP-Befehlen bearbeiten (je nach installierter Grafikbibliothek im JPG-, PNG- und/oder GIF-Format). Eine häufig gewünschte Funktion ist das Verkleinern von Bildern, um eine schneller zu ladende Vorschau bieten zu können.

Allerdings ist dies eine sehr rechenintensive Angelegenheit. Mit einer Galerie von 30 Bildern à 200 Kilobyte, die bei jedem Seitenaufruf neu skaliert werden, kann man einen Server leicht an die Lastgrenze bringen (und den Hoster zur Verzweiflung). Daher ist hier ein Caching-Mechanismus unerlässlich, der dafür sorgt, dass jedes Bild möglichst nur einmal berechnet wird (z.B. nach einem Upload oder einer Änderung), und ansonsten das gespeicherte Bild direkt ausliefert.

Christian Lamine zeigt in einem Tutorial, wie man so etwas realisieren kann. Ein ähnliches Script stellt die phpThumbnailer-Klasse dar.

Werden die Bilder in der Form image.php?datei=bild.jpg&x=40&y=30 referenziert, wird natürlich für jedes Bild bei jedem Seitenaufruf ein eigener PHP-Prozess gestartet (auch wenn dieser die Bilddaten nur per fpassthru() durchschiebt). Solange PHP als Webserver-Modul läuft, ist dies weniger dramatisch - gerade bei den großen Hostinganbietern wird aber oft die CGI-Variante von PHP eingesetzt, und da erzeugt dies eine beträchtliche Serverbelastung.

In der Regel wird zur Skalierung die Funktion ImageCopyResized() genutzt. Die Qualität ist hierbei je nach Auflösung und Farbtiefe nicht besonders gut. Ab PHP 4.0.6 gibt es zusätzlich die Funktion ImageCopyResampled() (benötigt mindestens GD 2.0.1), die dank Anti-Aliasing hübschere Ergebnisse liefert. Eine weitere Möglichkeit ist die Nutzung von externen Tools wie ImageMagick, das zwar sehr gute Ergebnisse produziert, bei den meisten Hostern aber vermutlich nicht verfügbar ist.

22.4. Warum werden beim Bearbeiten von Bildern mit den Image-Funktionen die Farben verfälscht?

Antwort von Guido Haeger

Die GD-Bibliothek, auf der die Image-Funktionen von PHP basieren, arbeitet intern standardmäßig mit einer Farbpalette von 256 Farben. Damit ist es kaum möglich ein Farb-Foto mit naturgetreuen Farben darzustellen. Abhilfe schafft die Verwendung der Funktionen imagecreatetruecolor() bzw. imagetruecolortopalette() , die jedoch eine Version der GD-Bibliothek >= 2.0.1 sowie eine PHP-Version >= 4.0.6. erfordern.

23. PDF-Dateien

23.1. Kann ich PDF-Dateien mit PHP erstellen?

Antwort von Kai Schröder

Ja. Es gibt für die Erstellung von PDF-Dateien mit PHP zwei Bibliotheken: die PDFlib von Thomas Merz und FastIO's ClibPDF . Für die kommerzielle Nutzung brauchst du aber eine spezielle Lizenz für die von dir verwendete Bibliothek. Details für die kommerzielle Nutzung von PDFlib findest du unter http://www.pdflib.com/pdflib/business.html. Den Lizenzvertrag zur Nutzung von ClibPDF findest du unter http://www.fastio.com/licensePlain.html.

Da das Erstellen von PDF-Dateien durch pixelgenaues Layouten sehr umständlich ist, hat Alexander Wirtz die Klassenbibliothek PC4P geschrieben, die die PDFLIB-Funktionen nutzt und mit der es wesentlich einfacher ist, Rahmen/Textblöcke etc. auf einem PDF-Dokument zu platzieren. Mit PC4P arbeitet man objektorientiert auf dem PDF-Dokument.

Eine weitere Variante PDF-Dateien zu erstellen ist die Verwendung einer der in PHP geschriebenen Klassen zur Generierung von PDF-Dateien wie FPDF, PDF-Klasse von RO&S oder der phppdflib. Diese Klassen bieten im Vergleich zu PDFlib und ClibPDF nur eingeschränkte - für viele Zwecke aber trotzdem ausreichende - Möglichkeiten.

Man kann PDF-Dateien aber auch ohne diese Bibliotheken oder Klasse erstellen - PDF-Dateien sind Text-Dateien, die bestimmte Steuersequenzen enthalten. Diese Steuersequenzen sind durch Adobe veröffentlicht worden und können wie normaler Text in eine Datei geschrieben werden. Adobe veröffentlicht die Details zum PDF-Format im Rahmen des Adobe Solutions Network (ASN). Die Referenz ist auch in Buchform erhältlich. In Zusammenarbeit mit Adobe sind im Addison-Wesley-Verlag bisher folgende Bücher erschienen:

Im PDF-Format ist die Verwendung der LZW-Kompression möglich. Diese ist durch das US-Patent No. 4,558,302 geschützt, welches der Firma Unisys Corporation gehört. Man benötigt also bei Erstellung von PDF-Dateien auf Basis der Datei-Spezifikationen eine Lizenz von Unisys, oder muss auf Komprimierung verzichten.

23.2. Welche Maßeinheit wird im PDF-Format verwendet?

Keywords: PDF | Einheit

Antwort von Kai Schröder

Laut PHP-Manual sind alle Längen- und Koordinatenangaben in Postscript-Punkten gemessen. Für gewöhnlich entsprechen 72 PostScript-Punkte 1 Inch (72 dpi, 1 Inch = 2,54cm), was jedoch von der Auflösung des Ausgabegeräts abhängt (Drucker verwenden meist 300 oder 600dpi).

Die Spezifikation von Adobe überläßt die Wahl der Maßeinteilung dem Entwickler. Eine PDF-Seite ist immer im Format DIN A4, also 297 Millimeter hoch und 210 Millimeter breit. Beim Anlegen einer neuen PDF-Seite muss eine Höhe und eine Breite in Punkten angegeben werden. Diese Angaben bilden dann den Maßstab für das Koordinatensystem.

Dieser berechnet sich wie folgt: Punktanzahl durch Kantenlänge (in Millimetern). Definiert man also die Breite der Seite mit 2100 Punkten, so ergibt sich ein Maßstab von 10 Punkten pro Millimeter (2100 Punkte geteilt durch 210 mm).

23.3. Kann ich für Höhe und Breite unterschiedliche Maßstäbe verwenden?

Keywords: PDF | Einheit

Antwort von Kai Schröder

Ja. Dies ist aber nicht zu empfehlen, da für die verzerrungsfreie Darstellung von Quadraten und Kreisen die unterschiedlichen Maßstäbe erst auf eine gemeinsame Basis gebracht werden müssen.

23.4. Kann ich bestehende PDF-Dateien als Template für dynamische Dokumente verwenden?

Keywords: PDF | Vorlage | Template

Antwort von Kai Schröder

Jein, nicht in jedem Fall. Ob sich PDF-Dateien als Template eignen, hängt von verschiedenen Faktoren ab. Zum ersten kann man mittels Adobe Acrobat ein PDF optimieren (siehe Kapitel F der PDF-Referenz). Dabei wird das PDF in seiner Struktur verändert und zum Teil komprimiert. Zum anderen sind für PDF-Dateien einige Sicherheitseinstellungen möglich, die die nachträgliche Veränderung nicht erlauben. Ein nicht optimiertes und ungeschütztes PDF läßt sich aber als Template verwenden.

Antwort von Thomas Weinert

Das Beispiel liest eine PDF-Datei ein und ersetzt vordefinierte Inhalte in Formularfeldern. Die Inhalte müssen dazu dem Schema [[$variable]] entsprechen.

Das Array $pdf_vars kann nach Bedarf gefüllt werden. Im Beispiel wird der Inhalt [[$foobar]] durch dclp-faq ersetzt.

<?php
//PDF-Datei mit Forumarfeldern
$pdffile = "sample_form.pdf";

//Variablen
$pdf_vars = array(
    'foobar'=>'dclp-faq'
);

//Funktion zum Ersetzen
function replace_pdf_var($match){
    if (isset($GLOBALS['pdf_vars'][$match[1]])) {
        return $GLOBALS['pdf_vars'][$match[1]];
    } else {
        return $match[1];
    }
}

if(file_exists($pdffile)){

    //Das Template binär öffnen
    if ($fp = fopen($pdffile, 'rb')) {
        $template = fread ($fp, filesize ($pdffile));
        fclose ($fp);

        //auf das Template einen Regex anwenden.
        $pdf = preg_replace_callback('#\[\[\$([^\]]+)\]\]#',
            'replace_pdf_var', $template);

        //HTTP-Header ausgeben
        header("Content-type: application/octet-stream");
        header("Content-Disposition: attachment; filename=\"$pdffile\"");
        header("Content-type: application/pdf");

        //das fertige PDF ausgeben
        echo $pdf;
   }
}
?>

23.5. Kann ich meine PDF-Dateien irgendwie schützen?

Keywords: PDF | Schutz | Verschlüsselung

Antwort von Kai Schröder

Ja. Adobe hat die Verschlüsselung von PDF-Dokumenten mittels einer Kombination aus MD5-Hashes und RS4 vorgesehen. Näheres findet sich in der PDF-Referenz: 3.5 Encryption.

23.6. Gibt es eine Rechteverwaltung für PDF-Dateien?

Keywords: PDF | Benutzerverwaltung

Antwort von Kai Schröder

Ja, allerdings gibt es nur zwei verschiedene Rollen. Es wird zwischen dem Besitzer und dem Benutzer unterschieden. Beide Benutzer verwenden unterschiedliche Passwörter. Der Besitzer hat nach Eingabe seines Passwortes volle Zugriffsrechte (einschließlich aller Veränderungen), die Rechte des Benutzers regeln die Einstellungen der Sicherheitsoptionen.

23.7. Wie erzeuge ich geschütze PDF-Dateien?

Keywords: PDF | Schutz | Verschlüsselung

Antwort von Kai Schröder

Die Details zur Vergabe der Rechte und die Generierung der Passwörter findest du in der PDF-Referenz: 3.5.2 Standard Security Handler.

23.8. Kann ich sicher feststellen, ob meine Besucher PDF-Dateien lesen können?

Keywords: PDF | Plugin

Antwort von Kai Schröder

Nein. Du kannst zwar den Inhalt der Variable $HTTP_ACCEPT prüfen, diese enthält aber nicht unbedingt die richtigen Werte. Eine Angabe wie Accept: */* (alle MIME-Types werden unterstützt) hilft nicht weiter.

Du solltest deine User darauf hinweisen, das sie den Adobe Acrobat Reader brauchen und ihnen eine Downloadquelle anbieten.

23.9. Welchen MIME-Type muss ich für PDF-Dateien verwenden?

Keywords: PDF | mime | header | Content-Type

Antwort von Kai Schröder

Der korrekte MIME-Type für PDF-Dateien lautet "application/pdf". Du kannst ihn mittels header("Content-type: application/pdf") an den Browser senden.

23.10. Wie sehe ich, ob meine PHP-Installation die PDF-Bibliotheken benutzen kann?

Keywords: PDF | extension

Antwort von Kai Schröder

Zur Laufzeit kannst du die Funktion extension_loaded() verwenden, um zu testen, ob die von dir benötigte Bibliothek zur Verfügung steht. Desweiteren kannst du Ausgabe von phpinfo() durchschauen.

23.11. Wie mache ich die PDF-Bibliotheken für meine Installation verfügbar?

Keywords: PDF | extension | installieren

Antwort von Kai Schröder

Wenn du deine PHP-Installation selbst kompiliert hast, dann lies bitte unter Komplette Liste der Konfigurationsoptionen nach, welche Optionen du für configure verwenden musst.

Desweiteren gibt es in der PHP-FAQ ein ganzes Kapitel zum Thema: Installation und Inbetriebnahme.

23.12. Warum stellt mein Internet Explorer statt der PDF-Datei eine leere Seite dar?

Keywords: PDF | leer

Antwort von Kerry W. Lothrop

Internet Explorer achtet in vielen Bereichen (fälschlicherweise) auf Dateienedungen statt auf den MIME-Typ. Bei Verwendung eines GET-Requests, insbesondere aber bei einem POST-Request kann es dazu kommen, dass nur eine leere Seite dargestellt wird. Folgende Möglichkeiten gibt es, diesen Bug zu umgehen:

  • Umbenennen der Dateiendung in .pdf und Konfigurieren des Servers, dass .pdf-Dateien durch PHP geparsed werden, z.B. durch die Apache-direktiven AddType oder ForceType.

  • Bei GET ohne Parameter: Anhängen der Zeichenkette /.pdf an die URL. Beispiel: /pdf.php/.pdf. Über diese Methode kann auch ein Dateiname spezifiziert werden, für den Fall, dass die Datei abgespeichert wird und der betreffende Browser die Content-Disposition-Direktive nicht kennt.

  • Alternativ: Anhängen der Zeichenkette ?.pdf an die URL. Beispiel: /pdf.php?.pdf.

  • Bei GET mit Parametern: Anhängen der Zeichenkette &.pdf an die URL. Beispiel: /pdf.php?file=termine&.pdf.

  • Bei POST: Schreiben des Ergebnisses in eine temporäre Datei mit der Endung .pdf und Weiterleitung per header() auf die Datei.

  • Alternativ bei kleinen PDF-Dateien (siehe Wie groß darf die Menge an Daten sein, die ich in einer Session speichern darf?): Die PDF-Datei in die Session schreiben und Weiterleitung auf eine PHP-Datei, die den Dateiinhalt aus der Session ausgibt, mit den oben beschriebenen GET-Methoden.

  • Das Umschreiben der URL auf der Serverseite, z.B. durch das Apache-Modul mod_rewrite.

24. Content Management Systeme

24.1. Was ist ein Content Management System? Warum ist es nützlich?

Antwort von Björn Schotte

Laut ContentManager ist ein Content Management System ein "Softwaresystem für das Administrieren von Webinhalten mit Unterstützung des Erstellungsprozesses basierend auf der Trennung von Inhalten und Struktur".

Bei sehr vielen Websites kommt es nicht darauf an, dass man besonders tolle PHP-Applikationen erstellt. Viel wichtiger ist, dass man das Tagesgeschäft erledigen kann, ohne durch fehlerhafte Programme, umständliche Bedienungen, Heranziehen von Softwareentwicklern vom normalen Ablauf gestört zu werden. Ein CMS unterstützt dieses Vorhaben, indem es eine Website in mehrere Bereiche aufteilt und über eine (meist webbasierte) Oberfläche den einzelnen Mitarbeitern zugänglich macht.

Das ermöglicht es auch nicht mit HTML versierten Mitarbeitern, Inhalte der Website zu pflegen. Den Rest, also die Integration des Inhalts in die Struktur, erledigt das CMS. Ein anderer Mitarbeiter, der zum Beispiel in HTML sehr fit ist, wird Zugriff auf das Layoutmodul des CMS haben und dort sogenannte HTML-Templates pflegen. In vielen CMSen sind diese Templates normaler HTML-Code, bei dem durch Schlüsselwörter definiert wird, an welcher Stelle welcher Inhalt gesetzt werden soll.

Sehr nützlich bei einem CMS sind auch noch die verschiedenen Zugriffsrechte für einzelne Benutzer(gruppen). Das macht eine Kontrolle möglich, zum Beispiel dass die Sekretärin nur im Inhaltsbereich Daten eingeben darf, aber keinen Zugriff auf das Layoutmodul des CMS hat.

Der Nutzer, der die Website oder Teile davon pflegt, kommt also gar nicht mehr in Kontakt mit z.B. FTP-Programmen. Scheinbar komplizierte Technik wird in eine übersichtliche Oberfläche verpackt, damit auch weniger versierte Nutzer die Inhalte pflegen können. Module wie zum Beispiel Mediendatenbanken, die Bilder, Sounds, Dateien etc. verwalten, machen eine Pflege selbst komplexer Sites zum Kinderspiel.

24.2. Welche PHP-basierten Content Management Systeme gibt es?

Antwort von Björn Schotte

Es gibt eine Fülle an PHP-basierten CMSen und Frameworks, so dass eine weitere Auflistung hier in der FAQ keinen Sinn machen würde.

Stattdessen empfehlen wir Ihnen den Einstieg bei contentmanager.de und dessen Produktfinder, mit dem man gezielt u.a. nach PHP-basierten CMSen suchen kann.

Weiterhin empfehlenswert ist die Übersicht über Content-Management-Systeme auf der Basis von PHP des PHP Magazins, in der sich CMS-Hersteller eintragen können, sowie der dazu begleitende Artikel aus der Ausgabe 2/2002. Das PHP Magazin wird regelmäßig eine aktualisierte Liste zur Verfügung stellen, CMS-Hersteller können sich auf der Homepage des PHP Magazins eintragen.

25. Häufig benötigte Codeschnipsel

25.1. Wie kann ich eine schummelsichere Abstimmung codieren?

Keywords: Abstimmung | Vote | Poll | Wahl | Umfrage

Antwort von Kristian Köhntopp

Man kann keine schummelsichere Abstimmung im Web realisieren, die nicht unfair berechtigte Abstimmende ausschließt, außer man treibt sehr großen Aufwand.

Typische Schutzmechanismen, die jedoch nur Pseudosicherheit geben, sind: Die Seite nimmt Parameter per POST, prüft den Referer und versucht einen Cookie zu setzen. Wer den Cookie hat, kann nicht mehr abstimmen. Zum Mogeln muss man der Seite also die richtigen Parameter aus dem Formular mit einem POST füttern, darf den Referer nicht vergessen und darf den Cookie nicht annehmen. Das war alles.

Eine etwas kompliziertere Absicherung wäre ein Formular, das als hidden-Parameter eine Challenge hat, die auch auf dem Server als Session-Variable verbleibt. Ein Abstimmungsergebnis wird dann für die aktuelle Session nur angenommen, wenn die richtige Challenge mit in den Parametern ist. Das macht das Schummeln per Script ein wenig schwieriger, aber nicht unmöglich (mit LWP in Perl oder libwww in C problemlos möglich, mit PHP ein Code mit den preg_*-Funktionen.

Mit SSL-Browser-Zertifikaten auf der Clientseite wäre es noch schwieriger zu fälschen, aber da gelangt man langsam in Regionen, wo es für den Wahlveranstalter wirklich teuer wird.

Auf der Basis der IP-Nummern kann man nicht arbeiten, da viele Benutzer dieselbe IP-Nummer zu haben scheinen, wenn sie über Proxy-Server hereinkommen oder Benutzer auf einem Mehrbenutzer-Rechner sind (unfairer Ausschluss). Andererseits ist es für einen geübten Hacker oder einen Provider sehr leicht, von einer ganzen Reihe unterschiedlicher IP-Nummern mehrfach abzustimmen.

25.2. Wie kann ich einen HTTP POST-Request absenden?

Keywords: POST | Request | Methode

Antwort von Kristian Köhntopp

Das Script muss einen Socket mit der Funktion fsockopen() zum Zielserver öffnen und auf diesem Socket dann einen HTTP POST-Request simulieren.

Anbei ein vollständiges Beispiel, das mit CGI-PHP auf der Unix-Kommandozeile verwendet werden kann. Das Script fälscht Einträge in einer Abstimmung auf dem Host www.linux.com, wo es für PHP als beste Scriptsprache stimmt.

#! ./php -q
<?php

function PostToHost($host, $path, $referer, $data_to_send) {
  $fp = fsockopen($host, 80);
  printf("Open!\n");
  fputs($fp, "POST $path HTTP/1.1\r\n");
  fputs($fp, "Host: $host\r\n");
  fputs($fp, "Referer: $referer\r\n");
  fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
  fputs($fp, "Content-length: ". strlen($data_to_send) ."\r\n");
  fputs($fp, "Connection: close\r\n\r\n");
  fputs($fp, $data_to_send);
  printf("Sent!\n");
  while(!feof($fp)) {
      $res .= fgets($fp, 128);
  }
  printf("Done!\n");
  fclose($fp);

  return $res;
}

$data = "pid=14&poll_vote_number=2";

printf("Go!\n");
$x = PostToHost(
              "www.linux.com",
              "/polls/index.phtml",
              "http://www.linux.com/polls/index.phtml?pid=14",
              $data
);

25.3. Wie kann ich einen HTTP POST-Request mit Datei-Upload absenden?

Keywords: POST | multipart | File | upload | Datei

Antwort von Andreas Bohne-Lang

Das Script muss einen Socket mit der Funktion fsockopen() zum Zielserver öffnen und auf diesem Socket dann einen HTTP POST-Request mit Datei-Upload simulieren.

Das Beispiel baut eine HTTP-Anfrage konform der multipart/form-data Beschreibung auf und sendet diese dann ab. Dabei wird eine ASCII-Datei eingelesen und übergeben.

Bei multipart/form-data werden die Daten so übergeben:

...
Content-type: multipart/form-data; boundary=---------------------------255141413922088
Content-Length: 6881

-----------------------------255141413922088
Content-Disposition: form-data; name="disk"

off
-----------------------------255141413922088
...
-----------------------------255141413922088--

Dieses Stück Beispielcode sendet einen solchen Request ab:

#!/usr/local/bin/php -q

<?php

// Andreas Bohne-Lang / a.bohne@dkfz.de / 22.10.2001 / GPL
// Ergaenzt: 19.9.2003

function PostToHost($host, $port, $path, $referer, $data_to_send)
{
     $dc = 0;
     $bo="-----------------------------305242850528394";

     $fp = fsockopen($host, $port, $errno, $errstr);
     if (!$fp) {
         echo "errno: $errno \n";
         echo "errstr: $errstr\n";
         return $result;
     }

     fputs($fp, "POST $path HTTP/1.0\n");
     fputs($fp, "Host: $host\n");
     fputs($fp, "Referer: $referer\n");
     fputs($fp, "User-Agent: Mozilla/4.05C-SGI [en] (X11; I; IRIX 6.5 IP22)\n");
     fputs($fp, "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*\n");
     fputs($fp, "Accept-Charset: iso-8859-1,*,utf-8\n");
     fputs($fp, "Content-type: multipart/form-data; boundary=$bo\n");

     foreach($data_to_send as $key=>$val) {
         $ds =sprintf("--%s\nContent-Disposition: form-data; name=\"%s\"\n\n%s\n", $bo, $key, $val);
         $dc += strlen($ds);
     }
     $dc += strlen($bo)+3;
     fputs($fp, "Content-length: $dc \n");
     fputs($fp, "\n");

     foreach($data_to_send as $key=>$val) {
         $ds =sprintf("--%s\nContent-Disposition: form-data; name=\"%s\"\n\n%s\n", $bo, $key, $val);
         fputs($fp, $ds );
     }
     $ds = "--".$bo."--\n";
     fputs($fp, $ds);

     while(!feof($fp)) {
         $res .= fread($fp, 1);
     }
     fclose($fp);

     return $res;
}

// Fuer eine ASCII-Datei kann man es so machen
// bei bin-Daten ueber fread gehen

$fa = @file("http://www.dkfz.de/spec/fix/file.pdb");

// Konkretes Beispiel - Eine Chemie-Datei
$xf="Content-Type: chemical/x-pdb\n\n". implode("", $fa);

$data["disk"]            = "on";
$data["file\"; filename=\"irgendwas.pdb"]        = $xf;
$data["smiles"]          = "";
$data["hadd"]            = "add";
$data["aroresolver"]     = "on";
$data["format"]          = "gif";
$data["interlace"]       = "1";
$data["width"]           = "600";
$data["height"]          = "400";
$data["atomcolor"]       = "Black";
$data["asymbol"]         = "xsymbol";
$data["hcolor"]          = "";
$data["csymbol"]         = "special";
$data["hsymbol"]         = "special";
$data["bondcolor"]       = "Black";
$data["bgcolor"]         = "White";
$data["border"]          = "12";
$data["bonds"]           = "8";
$data["wedges"]          = "1";
$data["dashes"]          = "1";
$data["crop"]            = "2";
$data["align"]           = "none";
$data["coord"]           = "0";
$data["imagemap"]        = "none";
$data["headercolor"]     = "Black";
$data["header"]          = "";
$data["footercolor"]     = "Black";
$data["footer"]          = "";
$data["commenttype"]     = "none";
$data["comment"]         = "";
$data["structure"]       = "none";

$x = PostToHost (
       "www2.chemie.uni-erlangen.de",
       80,
       "/cgi-bin/services/gifcreator.tcl",
       "http://www2.chemie.uni-erlangen.de/services/gifcreator/index.html",
       $data
      );


// Diesen Teil kann man bestimmt noch optimieren ;-)
$fp = fopen("struktur.gif", "wb");
$tok = 0;
for ($i=20; $i<strlen($x); $i++){
    if ((substr($x,$i,1)=="\n") || (substr($x,$i,1)=="\r")) {
        $tok++;
        if($tok>=3) break;
    } else {
        $tok=0;
    }
}
$i++; $i++;
printf(" %d %d %d \n", $i, strlen($x), strlen($x)-$i);
fwrite($fp, substr($x,$i,strlen($x)-$i));
fclose($fp);

$fp = fopen("struktur.xxx", "wb");
fwrite($fp, $x, strlen($x));
fclose($fp);

?>

25.4. Wie kann ich die IP des Users erfahren?

Keywords: IP | Adresse | Benutzer | Client | Browser | Proxy | Host

Antwort von Johannes Frömter

In der Umgebungs-Variablen REMOTE_ADDR steht die IP-Adresse des Rechners, der die Anfrage sendet. Dies ist nicht zwangsläufig der Rechner, an dem der User sitzt - es kann genausogut ein Proxy sein. Wenn der Benutzer in einer Firma mit mehr als 2-3 PCs sitzt, ist letzteres sogar sehr wahrscheinlich, aber es kann auch bei ganz normalen Provider-Endkunden so sein.

Folgendes Skript verwendet die Umgebungs-Variable REMOTE_ADDR und versucht, den Hostnamen zur IP mit der Funktion gethostbyaddr() zu ermitteln (sog. reverse lookup).

<?php
// IP bestimmen
$ip = getenv('REMOTE_ADDR');

// IP auflösen und Host bestimmen
$host = gethostbyaddr($ip);
?>

Kommt die Verbindung über einen Proxy zustande, kann es sein, dass dieser die IP "seines" Clients im HTTP-Header weitergibt. Der verbreitete Proxy squid beispielsweise nennt diesen Header X-Forwarded-For, dessen Inhalt dann (wie andere HTTP-Header auch) in einer Umgebungsvariablen zur Verfügung gestellt wird (HTTP_X_FORWARDED_FOR).

25.5. Wie kann ich die Performance zweier Befehle vergleichen?

Antwort von Martin Jansen

<?php
  function start_timer($event) {
    printf("timer: %s<br>\n", $event);
    list($low, $high) = explode(" ", microtime());
    $t = $high + $low;
    flush();

    return $t;
  }

  function next_timer($start, $event) {
    list($low, $high) = explode(" ", microtime());
    $t    = $high + $low;
    $used = $t - $start;
    printf("timer: %s (%8.4f)<br>\n", $event, $used);
    flush();

    return $t;
  }

  $t = start_timer("start Befehl 1");

  /* Hier den ersten Befehl einfuegen */

  $t = next_timer($t, "start Befehl 2");

  /* Hier den zweiten Befehl einfuegen */

  $t = next_timer($t, "finish");
?>

Möchte man zum Beispiel den Performance-Unterschied zwischen mysql_fetch_row und mysql_fetch_array bestimmen, fügt man die beiden Befehle an die beiden mit Kommentaren versehenen Stellen im Skript ein.

Antwort von Sebastian Bergmann

Eine Alternative zur oben dargestellten Methode der Performance Messung bieten die PEAR Klassen Benchmark_Timer und Benchmark_Iterate.

Die Benchmark_Timer Klasse stellt die benötigte Funktionalität zur Verfügung, um Marken zu setzen, und die zwischen zwei Marken verstrichene Zeit zu berechnen.

<?php
  // PEAR::Benchmark_Timer inkludieren
  require_once "Benchmark/Timer.php";

  // Timer Instanz erzeugen
  $Timer = new Benchmark_Timer;

  // Marke "Beginn For-Schleife" setzen
  $Timer->setMarker("Beginn For-Schleife");

  for ($i = 0; $i < 10000; $i++) { }

  // Marke "Ende For-Schleife" setzen
  $Timer->setMarker("Ende For-Schleife");

  // Zeit zwischen den beiden Marken berechnen
  print $Timer->timeElapsed("Beginn For-Schleife","Ende For-Schleife");
?>

Die Benchmark_Iterate Klasse leitet sich von der Benchmark_Timer Klasse ab und ermöglicht die wiederholte Ausführung einer Funktion, um so die mittlere Ausführungsgeschwindigkeit derselben zu ermitteln.

<?php
  // Funktion, die wir benchmarken wollen
  function foo()
  {
    print "bar<br>";
  }

  // PEAR::Benchmark_Iterate inkludieren
  require_once "Benchmark/Iterate.php";

  // Benchmark Instanz erzeugen
  $Benchmark = new Benchmark_Iterate;

  // Benchmark ausführen
  $Benchmark->run(100, "foo");

  // Ergebnis ausgeben
  $result = $Benchmark->get();
  print "Mittlere Ausführungsgeschwindigkeit: " . $result["mean"];
?>

Die run()-Methode erlaubt einen dritten (vierten, fünften, ...) Parameter, welcher der zu testenden Funktion als Argument übergeben wird.

25.6. Wie kann ich den Inhalt eines Verzeichnisses samt dem Inhalt aller Unterverzeichnisse ausgeben?

Antwort von Guido Haeger

Um nicht nur den Inhalt des aktuellen Verzeichnisses, sondern auch den Inhalt aller Unterverzeichnisse ausgeben zu können, muss man eine rekursive Funktion verwenden. Diese ruft sich bei Bedarf selbst auf. Im nachfolgenden Beispiel durchläuft die Funktion show_dir jeweils das aktuelle Verzeichnis. Wird Datei gefunden, wird der Dateiname ausgegeben. Findet die Funktion ein Verzeichnis, dann wird der Verzeichnisname fett ausgegeben und die Funktion ruft sich mit dem Unterverzeichnis als Parameter selbst wieder auf.

<?PHP

function show_dir($dir, $pos=2)
{
    if($pos == 2)
    {
        echo "<hr><pre>";
    }

    $handle = @opendir($dir);
    while ($file = @readdir ($handle) !== false)
    {
        if (preg_match("=^\.{1,2}$=", $file))
        {
            continue;
        }

        if(is_dir($dir.$file))
        {
            printf ("% ".$pos."s <b>%s</b>\n", "|-", $file);
            show_dir($dir.$file."/", $pos + 3);
        }
        else
        {
            printf ("% ".$pos."s %s\n", "|-", $file);
        }
    }
    
    @closedir($handle);

    if($pos == 2)
    {
        echo "</pre><hr>";
    }

}

show_dir("special/");

?>

25.7. Wie kann ich aus einem Zahlenbereich von x bis y, zufällig n Zahlen auswählen, so dass keine Zahl doppelt vorkommt?

Keywords: Zufall | Zahlen | Duplikat | einmalig

Antwort von Guido Haeger

Mit der Funktion array_rand() . In PHP3 steht diese Funktion nicht zur Verfügung. Nachfolgende Funktion liefert hier bessere Ergebnisse, als shuffle() auf das Array anzuwenden und dann die n Zahlen zu verwenden.

<?PHP

function generate_numbers($min, $max, $anz)
{
     $array = range($min, $max);
     srand ((double)microtime()*1000000);
     for($x = 0; $x < $anz; $x++)
     {
          $i = rand(1, count($array))-1;
          $erg[] = $array[$i];
          array_splice($array, $i, 1);
     }
     return $erg;
}

// 5 eindeutige Zahlen im Bereich von 1 bis 100 ermitteln
$zufalls_array = generate_numbers(1, 100, 5);
echo join("; ", $zufalls_array);

?>

25.8. Wie kann ich zählen, wie oft auf einen Link geklickt wurde?

Keywords: Link | Counter

Antwort von Martin Jansen

Dem Skript liegt folgende Struktur der MySQL-Tabelle zugrunde:

mysql> describe counter;
+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned |      | PRI | 0       | auto_increment |
| url   | char(255)        |      |     |         |                |
| count | int(11)          |      |     | 0       |                |
+-------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

Das Feld url enthält die URL, die aufgerufen wird. Das Feld count enthält die Anzahl der Klicks auf url.

<?php

// Zugangsdaten fuer die Datenbank
// Diese sollten der Sicherheit halber
// in ein Verzeichnis außerhalb des
// Document-Root ausgelagert werden.

$host     =    "localhost";
$user     =    "user";
$pass     =    "demo_password";

$datab    =    "demo_db";
$table    =    "counter";

// Die per GET übergebene URL einlesen und datenbanksicher machen
$url = addslashes($_GET['url']);

// Verbindung zum MySQL-Server aufbauen
$db = @mysql_connect($host, $user, $pass);

if ($db) {
    if (@mysql_select_db($datab, $db)) {
        // Eintrag fuer die übergebene URL um 1 erhöhen.
        $query = "UPDATE $table SET count = count + 1 WHERE url = '$url'";
        $result = @mysql_query($query);

        // Noch kein Eintrag für die URL vorhanden?
        if (mysql_affected_rows() == 0) {
            $sql_insert = "INSERT INTO $table (url, count) VALUES ('$url', '1')";
            @mysql_query($sql_insert);
        }
    }
}

// Auf übergebene URL weiterleiten
Header("Location: " . $_GET['URL']);

?>

Anwendungsbeispiel:

<a href="count.php?url=http://www.martin-jansen.de">Link</a>

Als Parameter für die Datei count.php wird die URL übergeben, auf die weitergeleitet werden soll. In count.php wird nun der Datensatz in der Tabelle, der $url als Wert für das Feld url enthält um 1 erhöht und es wird auf die neue URL weitergeleitet.

25.9. Wie kann ich das Datum der letzten Änderung einer Datei erfahren?

Keywords: Datei | Datum | Aenderung | Zeit

Antwort von Daniel T. Gorski

<?php
// Modifikationszeit von (Wie heiße ich?)
$unixTime = filemtime($HTTP_SERVER_VARS["PATH_TRANSLATED"]);

if ($unixTime) {
    echo "Letzte Änderung: ". date("d M Y, H:i:s", $unixTime);
} else {
    echo "Datei existiert nicht! Falscher Pfad?";
}
?>

Möchte man unter UNIX-Derivaten das Datum der letzten Änderung der zugehörigen Inodes (z.B. Änderungen der Filesystem-Rechte) erfahren, so ist die Funktion filectime() zu benutzen.

25.10. Wie biete ich meine Seiten mehrsprachig an?

Keywords: Sprache | international | i18n

Antwort von Martin Jansen

Auf der Seite http://www.php-center.de/artikel/i18n.php3 findet man einen Artikel, der sich mit der Internationalisierung von PHP-generierten Seiten befasst.

25.11. Wie kann ich ermitteln, wieviele Besucher gerade meine Seite betrachten?

Keywords: User | Besucher | Homepage | online | HTTP

Antwort von Markus Dobel

Es gibt keine zuverlässige Methode, dies herauszufinden, da HTTP ein zustandsloses Protokoll ist und daher keine Stati "eingeloggt, ausgeloggt" wie bei FTP, Telnet etc. vorhanden sind. Der Client (Browser) baut eine Verbindung zum Webserver auf, fordert eine Seite mit all ihren Elementen (Bildern, JavaScript etc.) an und beendet danach die Verbindung wieder. Es entsteht also immer nur eine punktuelle, temporär andauernde Verbindung zwischen dem Webserver und dem Client des Anwenders.

Mit Hilfe von PHPlib und den sess_*-Funktionen von PHP4 kann man einen Besucher zwar wiedererkennbar machen, man kann damit jedoch nur feststellen, wann ein Besucher zuletzt eine Seite angefordert hat.

Wie lange er diese liest und daher noch "auf der Seite online" ist, kann man nicht herausfinden. Er könnte 30 Minuten an dem Text einer Seite lesen oder aber auch das Browserfenster direkt nach Anforderung der Seite schließen; der Server weiss nur, wann er die Seite wem ausgeliefert hat.

Es gibt einige Denkansätze, wie man diese fehlende Funktionalität nachbauen könnte, welche jedoch alle von der Zusammenarbeit mit dem Browser des Besuchers abhängig sind, um halbwegs zuverlässig zu arbeiten und/oder nur Näherungsweise an die reellen Gegebenheiten herankommen.

Ein paar davon bauen auf Javascript auf, welches man von vornherein ausklammern sollte, da nicht jeder Browser Javascript beherrscht und auch angeschaltet hat. Diese Methode ist also unzuverlässig.

Eine weitere Idee basiert darauf, einen kleinen "Blindframe" auf der Seite zu platzieren, welcher regelmäßig automatisch per META-Tag neugeladen wird. Auch wird darauf vertraut, dass der automatische Refresh wirklich bei jedem Besucher ausgeführt wird und man verpflichtet seine Besucher dazu, einen Browser zu benutzen, der Frames beherrscht. Darüberhinaus vergrault man sich auf Dauer einige Besucher, die die ständige Netzaktivität des Browsers irritiert oder nervt.

Zuletzt könnte man noch auswerten, wieviele Benutzer innerhalb der letzten x Minuten eine Seite angefordert haben und daher Annahmen darüber treffen, ob diese Besucher noch da sind. Auch dies ist keine zuverlässige Aussage.

Zusammenfassend lässt sich also sagen, dass die Aussage "x User online" (wie sie auf vielen Sites zu finden ist) reines Blendwerk sind. Es ist technisch nicht möglich, diese Aussagen zu treffen.

25.12. Wie überprüfe ich Hyperlinks auf ihre Gültigkeit?

Keywords: Link | HTTP | 404 | Test | Pruefung

Antwort von Johannes Frömter

Indem man mit fsockopen() eine Verbindung zum Webserver herstellt, einen HTTP-HEAD-Request auf die zu überprüfende Ressource absetzt und die Antwort auswertet.

Wer dies nicht selber programmieren will, kann Scripts wie z.B. die Funktion phpLinkCheck benutzen.

25.13. Wie erzeuge ich Excel-Dateien mit PHP?

Keywords: Excel | Download | CSV | Import

Antwort von Johannes Frömter

"Richtige" Excel-Dateien lassen sich kaum erzeugen, da .xls ein binäres, undokumentiertes proprietäres Microsoft-Format ist, das zudem von Version zu Version verändert wird.

Stattdessen kann man aber Textdateien erzeugen, deren Werte (Spalten) mit Tabulatoren und deren Zeilen mit CRLF (\r\n) getrennt sind, und diese Excel als .xls-Datei "unterschieben". Wichtig ist hierbei, als Dateityp tatsächlich eine Excel-(.xls)-Datei zu deklarieren - obwohl Excel diverse Importformate wie .csv (comma separated value) kennt, funktioniert es damit, vermutlich mangels definiertem Content-Type, nicht wie gewünscht.

header("Content-Type: application/vnd.ms-excel");
header("Content-Disposition: inline; filename=\"excel.xls\"");
readfile($filename);                       // Datei 1:1 durchreichen
// echo "Titel (A1)\r\nA2\tB2\r\nA3\tB3";  // ODER mit PHP erzeugen

Anmerkung: Trotz der Angabe inline bietet der Browser i.d.R. einen Dialog an, der das direkte Öffnen oder aber das Speichern der Datei ermöglicht. Die Angabe attachment dagegen führte zu kuriosen, nicht brauchbaren Ergebnissen...

Ein gänzlich anderer Weg, Excel-Dateien zu erzeugen, führt über die COM-Funktionen von PHP4. Damit kann man eine Excel-Instanz direkt ansteuern und so u.a. eine echte .xls-Datei generieren. Wer das ausprobieren will, der lese diesen Artikel und besorge sich die Excel Class von Alain Samoun.

Wer auch auf Unix-Systemen echte Excel-Dateien erstellen will, sollte sich die Klasse Biff von Christian Novak ansehen. Hiermit ist es möglich direkt das binäre Excel-Format zu schreiben. Verschiedene Schriftformatierungen, sowie Zellenformatierungen, Kommentare und sogar Kopf- und Fusszeile werden unterstützt.

Im PEAR-Projekt entsteht eine Klasse zum Erzeugen von Excel-Dokumenten. Sie steht unter PEAR::Package::Spreadsheet_Excel_Writer zum Download verfügbar.

25.14. Wie kann ich prüfen, ob eine IP-Adresse in einem bestimmten Bereich liegt?

Keywords: IP | Range | Adresse

Antwort von Johannes Frömter

PHP4 bietet die Funktion ip2long() , mit der man eine IP-Adresse in Punktschreibweise ("dotted quad") in die numerische Form umrechnen kann, was einen einfachen größer/kleiner-Vergleich erlaubt. Der Haken daran: Bei PHP beträgt der Maximalwert eines Integer-Typen etwas über 2 Milliarden (genau: 2.147.483.647, vorzeichenbehafteter 32-Bit-Wert), was für die Umrechnung von IP-Adressen nicht ausreicht; bei 127.255.255.255 ist Schluss, danach wechselt das Vorzeichen.

Wandelt man die IP dagegen in einen String um, gibt es dieses Limit nicht. Dann kann man einfach mittels if() oder strcmp() vergleichen:

if (ip2str($start) <= ip2str($ip)
    AND
    ip2str($ip) <= ip2str($ende))
...

function ip2str($ip) {
    $ip = preg_replace("/(\d{1,3})\.?/e",
                       'sprintf("%03d", \1)',
                        $ip);
    return (string)$ip;
}

25.15. Wie überprüfe ich, ob eine Zahl gerade oder ungerade ist?

Keywords: gerade | ungerade | pruefen

Antwort von Martin Jansen

Der einfachste Weg dies zu testen, ist die Zahl durch 2 zu teilen und zu überprüfen, ob die Division einen Rest zurückgibt.

In PHP kann man dies wie folgt realisieren:

<?php
if ($zahl % 2 != 0) {
    echo "Der Wert der Variablen \$zahl ist ungerade";
} else {
    echo "Der Wert der Variablen \$zahl ist gerade";
}
?>

25.16. Wie wandle ich Sekunden in Tage/Stunden/Minuten/Sekunden um?

Keywords: Stunde | Minute | Sekunde | Zeit | Intervall

Antwort von Johannes Frömter

Normalerweise ist für die Umsetzung von Sekunden in ein "lesbares Format" der Befehl date() zuständig, der einen Unix-Timestamp (vergangene Sekunden seit dem 1.1.1970) verarbeitet. Möchte man jedoch eine Anzahl Sekunden nicht als absolutes Datum, sondern als Intervall in Tagen, Stunden, Minuten und Sekunden darstellen, hilft folgende Funktion:

function intervall($sek) {
    $i = sprintf('%d Tag%s, %d Stunde%s,'.
            ' %d Minute%s und %d Sekunde%s',
            $sek / 86400,
            floor($sek / 86400) != 1 ? 'e':'',
            $sek / 3600 % 24,
            floor($sek / 3600 % 24) != 1 ? 'n':'',
            $sek / 60 % 60,
            floor($sek / 60 % 60) != 1 ? 'n':'',
            $sek % 60,
            floor($sek % 60) != 1 ? 'n':''
         );
    return $i;
}

echo intervall(99114);

25.17. Wie stelle ich Tabellenzeilen abwechselnd farbig dar?

Keywords: Tabelle | Zeile | Zelle | Farbe | Hintergrund

Antwort von Johannes Frömter

In der folgenden Funktion bgcolor() kann man beliebig viele Farben im Array $col definieren, die bei jedem Aufruf der Reihe nach berücksichtigt werden. Optional kann die Funktion mit einem Integer-Wert aufgerufen werden (bgcolor(n)), um immer n aufeinander folgende Zeilen derselben Farbe zu erhalten.

function bgcolor($row = 1) {
    static $i;
    static $col = array('#FFDDDD',
                        '#DDFFDD',
                        '#DDDDFF'
                       ); // etc.
    $bg = $col[(int)($i + .00000001)];
    $i += 1 / $row;
    if ($i >= count($col)) $i = 0;
    return $bg;
}

// Ausgabe einer Tabellenzeile (in einer Schleife):
printf("<tr bgcolor='%s'><td>...</td></tr>\n", bgcolor(2));

Antwort von Clemens Koppensteiner

Besonders, wenn man die Zeilen sowieso mitzählt, kann man den gleichen Effekt leicht mittels des Modulo-Operators (%) und CSS erreichen.

printf("<tr class='row%s'><td>...</td></tr>\n", $line % 2);

Und im Stylesheet:

row0 {background-color:#FFDDDD}
row1 {background-color:#DDFFDD}

Zur Erklärung: Der Modulo-Operator gibt den Rest der Division der zwei Werte zurück. 13 % 5 ist also zum Beispiel gleich 3 (Mathematisch korrekt ausgedrückt: 13 ist kongruent 3 modulo 5). $line % 2 gibt also abwechselnd 0 und 1 zurück.

25.18. Wie kann ich prüfen, ob eine bestimmte ICQ UIN online ist?

Keywords: ICQ | Mirabilis | UIN | online | offline | disabled

Antwort von Johannes Frömter

Es gibt einige fertige Scripte, die mit Hilfe von PHP testen können, ob eine bestimmte UIN online ist. Die meisten dieser Scripte funktionieren allerdings nicht mehr, weil ICQ (wieder einmal) etwas verändert hat. Der Server liefert auch weiterhin Redicts auf die Bildchen, allerdings hat sich die URL geändert.

Das folgende Script wertet die Redirects aus:

function GetICQ($uin) {
    if (!is_numeric($uin)) return FALSE;

    $fp = fsockopen('status.icq.com', 80, &$errno, &$errstr, 8);
    if (!$fp) return FALSE;

    $request = "HEAD /online.gif?icq=$uin HTTP/1.0\r\n"
              ."Host: web.icq.com\r\n"
              ."Connection: close\r\n\r\n";
    fputs($fp, $request);

    do {
        $response = fgets($fp, 1024);
    }
    while (!feof($fp) && !stristr($response, 'Location'));

    fclose($fp);

    if (strstr($response, 'online1')) return 'online';
    if (strstr($response, 'online0')) return 'offline';
    if (strstr($response, 'online2')) return 'disabled';
    // disabled meint, dass der Benutzer eingestellt hat, dass sein 
    // Status im Web nicht angezeigt wird.
    
    return FALSE;
}

// Aufruf:
echo GetICQ(12423456);

25.19. Wie kann ich prüfen, ob eine bestimmter Yahoo! Messenger User online ist?

Keywords: Yahoo! | Messenger | online | offline | disabled

Antwort von Timm Friebe

Der Check, ob ein User des Yahoo! Messengers online ist, funktioniert analog zu dem Check auf ICQ UINs.

// Contributed by Maik Große, http://about-php.de/
function GetYahoo($_yahoo = '') {
  if (empty($_yahoo)) return true;
  
  $response = "";
  if ($fp = fsockopen('opi.yahoo.com', 80, &$errno, &$errstr, 8)) {
    $request = "HEAD /online?u=".$_yahoo."&m=t&t=0 HTTP/1.0\r\nHost: opi.yahoo.com\r\nConnection: close\r\n\r\n";
    fputs($fp, $request);
    do {
      $response = fgets($fp, 1024);
    }
    while (!feof($fp) && !stristr($response, 'Location'));
    fclose($fp);
  }
  if (strstr(strtoupper($response), 'NOT ONLINE')) return false;
  if (strstr(strtoupper($response), 'ONLINE')) return true;
  return false;
}

// Aufruf:
echo "Yahoo User: 12423456 is ".((GetYahoo('12423456')) ? "online" : "not online")." ...";

25.20. Wie kann ich das Alphabet aufzählen?

Keywords: Alphabet | ABC | Buchstabe | Array | AZ

Antwort von Johannes Frömter

Immer wieder kommt es vor, dass man die Buchstaben des Alphabets in einem Array benötigt. Natürlich muss man das nicht von Hand machen:

for ($i = 65; $i <= 90; $i++) {
    $alphabet[] = chr($i);
}

Für Großbuchstaben lässt man die for-Schleife von 65 bis 90 laufen, für Kleinbuchstaben von 97 bis 122. Die Zahlen (ASCII-Codes) werden durch die Funktion chr() in die entsprechenden Buchstaben umgewandelt und im Array $alphabet abgelegt.

Es geht aber auch noch kürzer - seit PHP 4.1.0 akzeptiert die Funktion range() auch Buchstaben als Bereichsangabe:

$alphabet = range('A', 'Z');

26. Häufig nachgefragte Standardscripte

26.1. Wo finde ich ein Script, das "xyz" kann?

Keywords: Script | fertig | Sammlung | suchen | finden

Antwort von Martin Jansen

Es gibt im Internet eine Vielzahl von Seiten, die Scripte kostenlos zum Download anbieten, die viele verschiedene Zwecke erfüllen.

Typische Anlaufstellen bei der Suche nach fertigen Scripten sind:

Ebenso ist es hilfreich, bei Freshmeat nach php zu suchen, dort finden sich sehr viele PHP-Scripte. Eine weitere Möglichkeit, fertige PHP-Scripte zu finden, ist die Suche bei SourceForge nach php.

26.2. Wie kann ich eine Volltextsuche realisieren?

Antwort von Kristian Köhntopp

Eine Suchmaschine fuer Volltext wird man in den meisten Faellen nicht in PHP und mit einer SQL-Datenbank programmieren, sondern sinnvollerweise für diesen Anwendungszweck spezialisierte Software verwenden. SQL-Datenbanken sind nur dann optimal eingesetzt, wenn die Art der Daten und die Art der Anfragen den Einsatz von Indices möglich machen. Die meisten SQL-Datenbanken sind von Haus aus nicht besonders gut eingerichtet, um Indices über Volltext verwalten zu können: Zum einen können viele SQL-Datenbanken BLOB und TEXT-Felder gar nicht indizieren. Zum anderen können die meisten Datenbanken vorhandene Indices nicht nutzen, wenn der Suchausdruck nicht ohne Wildcard am vorderen Rand der Spalte verankert ist, d.h. wenn die Suche die Form LIKE '%suchwort' hat. MySQL bietet ab der Version 3.23.23 die Möglichkeit, einen Volltextindex anzulegen. Eine Anleitung dazu befindet sich in Wie realisiere ich eine Volltextsuche mit MySQL?

Einige populäre Volltextsuchmaschinen:

In der Newsgroup wurde der folgende Text zum Studium empfohlen: Managing Gigabytes; Compressing and Indexing Documents and Images, Ian H. Witten, Alistair Moffat, Timothy C. Bell; Morgan Kaufmann Publishers.

26.3. Wie kann ich mit PHP News lesen und schreiben?

Keywords: Newsgroup | IMAP | NNTP | News | Webnews

Antwort von Kristian Köhntopp

Mit Hilfe der IMAP-Bibliothek und IMAP-Funktionen kann man auch auf Newsserver zugreifen.

Auf den Webseiten von Floh findet man einen in PHP geschriebenen Newsclient. Dieser verwendet jedoch nicht die IMAP-Funktionen, sondern bildet diese Funktionen manuell nach, da das IMAP-Modul bei vielen PHP-Installationen von Webhostern nicht verfügbar ist.

26.4. Wie kann ich einen Onlineshop mit PHP realisieren?

Keywords: Shop

Antwort von Kristian Köhntopp

Fertige Onlineshoplösungen in PHP:

26.5. Welche in PHP realisierte Foren gibt es?

Keywords: Forum | Thread | Diskussion | Community | Board

Antwort von Martin Jansen

Wenn man ein Forum in PHP selbst schreiben möchte, kann der Artikel "Threaded Discussion with PHP/MySQL" als Grundlage dienen.

Man kann alternativ auf die folgenden in PHP geschriebenen Foren zurückgreifen:

26.6. Welche Webmail-Oberflächen in PHP gibt es?

Keywords: Webmail | EMail | IMAP | POP3 | GMX

Antwort von Johannes Frömter

Es gibt zahlreiche fertige PHP-Lösungen für den Zugriff auf EMail-Accounts per Weboberfläche. Viele von ihnen benutzen die IMAP -Library, die nicht bei jedem Provider installiert ist. Für das Verarbeiten und Speichern der Mails wird oft eine Datenbank benötigt, und zum Versenden von Mails wird teilweise das lokal installierte Mailprogramm benutzt - diese Voraussetzungen sollten bei der Auswahl also berücksichtigt werden.

  • AeroMail - IMAP-Webmail-Client für PHP3 oder PHP4. PHP muss als Apache-Modul laufen. (Funktioniert nicht mit register_globals = off)

  • Basilix - IMAP- und MySQL-basierende Applikation in PHP. (Funktioniert nicht mit register_globals = off)

  • IMP - IMP ist der Webmail-Teil des Horde-Projektes. Die IMAP-Bibliothek von PHP sowie MySQL oder PostgreSQL sind nötig. Versionen für PHP3 (mit PHPLIB) und PHP4 vorhanden.

  • Instant Web Mail - einfacher, mehrsprachiger POP3-Mailclient für PHP ab Version 4.0.3 ohne IMAP-Modul.(Funktioniert nicht mit register_globals = off)

  • NOCC - In PHP4 geschriebener Webmail-Client für IMAP- und POP3-Accounts, keine Datenbank nötig, unterstützt zahlreiche Sprachen.(Funktioniert nicht mit register_globals = off)

  • phpop - Webmailer basierend auf PHP3, MySQL und PHPLIB. (Funktioniert nicht mit register_globals = off)

  • PHPost - Reiner POP3-Client für PHP4, kommt ohne Datenbank, IMAP-Modul und JavaScript aus. Unterstützt mehrere Sprachen. (Funktioniert nicht mit register_globals = off)

  • popper - POP3-Webmail-Client für PHP4 und MySQL. (Funktioniert nicht mit register_globals = off)

  • Postaci - Türkischer Webmailer (mehrsprachenfähig) für PHP4, SQL-Datenbank nötig, POP3- und IMAP-Unterstützung. (Funktioniert nicht mit register_globals = off)

  • rymo - Kleiner POP3-Webmail-Client für PHP3/4. (Funktioniert nicht mit register_globals = off)

  • SquirrelMail - Leistungsfähiger Webmail-Client für PHP4. Nur geeignet für IMAP-Mailboxen, benötigt dennoch nicht die IMAP-Bibliothek von PHP4! Plugin-fähig, zahlreiche Erweiterungen vorhanden.

  • TWIG - Groupware-Tool mit EMail-Funktion (IMAP), läuft auf PHP3 und PHP4, braucht eine Datenbank (MySQL oder PostgreSQL). (Funktioniert nicht mit register_globals = off)

26.7. Welche kostenlose Portalsoftware in PHP gibt es?

Keywords: Portal | Homepage | Community | News

Antwort von Martin Jansen

Es gibt einige fertige Systeme, mit denen sich relativ schnell eine kleine Portalseite mit diversen Funktionen aufziehen lässt:

26.8. Welche Lösungen gibt es, um Diagramme zu erstellen?

Antwort von Sebastian Bergmann

Es gibt einige fertige Systeme, die einem die Funktionen der diversen Grafikbibliotheken von PHP so abstrahieren, dass auch die Erstellung komplexer Diagramme recht einfach von der Hand geht:

26.9. Wie kann ich ID3-Tags von MP3-Dateien lesen/schreiben?

Keywords: Script | MP3 | ID3 | Tag | Datei | lesen | schreiben

Antwort von Johannes Frömter

Es gibt etliche fertige Scripte, die ID3-Tags lesen und schreiben können:

Die ID3-Tags Version 1 (ID3v1) sind relativ simpel, da die Länge der einzelnen Felder genau definiert ist und der Datenblock einfach die letzten 128 Byte der Datei belegt. ID3v2-Tags dagegen sind wesentlich komplexer, da sich diese am Anfang der Datei befinden und zudem eine variable Länge aufweisen (sie können bis zu 256 MB groß sein, Texte in Unicode, Links, ja sogar Bilder enthalten); die genaue Spezifikation gibt es unter http://www.id3.org/.

26.10. Wie kann ich eine whois-Abfrage mit PHP realisieren?

Keywords: Script | whois | Domain | Abfrage | NIC | TLD

Antwort von Johannes Frömter

Um den Status einer Domain in Erfahrung zu bringen, stellt das jeweilige NIC (Network Information Center) einen sog. whois-Dienst für die von ihm verwaltete TLD (Top Level Domain, .com, .de, .info etc.) zur Verfügung. Dabei gibt es in der Praxis zwei Probleme: erstens sind die Adressen der whois-Server nicht wirklich einheitlich, und zweitens variiert das Ausgabe-Format von Anbieter zu Anbieter sehr stark - der eine liefert z.B. die komplette Anschrift des Domaininhabers, der andere sagt nur "besetzt" oder "frei". Die meisten whois-Scripte werten deshalb die Antwort des Server nicht aus, sondern liefern sie 1:1 als Text zurück.

Hier eine Liste von in PHP programmierten whois-Scripten:

  • MWhois bringt eine ausführliche Liste von NICs mit und parst die Antworten der Server nach Strings wie "No entries found", um den Status zu ermitteln. Kann durch Templates an verschiedene Sprachen angepaßt werden.

  • whoislookup arbeitet überwiegend nach dem Schema whois.nic.tld. Die Server-Antwort wird ungeparst präsentiert.

  • whois2 arbeitet mit einer eigenen NIC-Liste. Für ausgewählte NICs gibt es Erweiterungen, die das Parsen der Server-Antworten erledigen.

  • whoisclass überläßt die ganze Arbeit dem Server whois.geektools.com, der seinerseits die tatsächlichen whois-Server abfragen muss. Für stark frequentierte Seiten ist es ratsam, den GeekTools Proxy auf dem eigenen Server zu installieren, um den Traffic in Grenzen zu halten.

26.11. Welche Groupware-Tools in PHP gibt es?

Antwort von Johannes Frömter

Groupware-Tools bieten Funktionen wie Gruppenterminkalender, Projektmanagement, verwalten Kontakte und Todo-Listen etc. pp. Die folgenden Groupware-Applikationen sind in PHP programmiert:

27. Guter Code

27.1. Halte Code links. Verwende Wächter statt Schachtel-if

Keywords: Schleife | Funktion | If

Antwort von Kristian Köhntopp

In der strukturierten Programmierung erzeugt man Schleifen ohne Schleifenkurzschlüsse mit continue oder break und Funktionen mit genau einem Funktionsausgang durch return. Dadurch entsteht häufig Code mit sehr vielen geschachtelten Abfragen.

Meistens versucht man vor der eigentlichen Nutzlast einer Schleife oder einer Funktion eine Reihe von Vorbedingungen zu testen, die für den erfolgreichen Einsatz der Nutzlast sichergestellt sein müssen. Der generierte Code sieht dann wie folgt aus:

if (vorbedingung) {
  if (vorbedingung2) {
    if (vorbedingung3) {
      doit(); // Nutzlast
    } else {
      handle_error3();
  } else {
    handle_error2();
  }
} else {
  handle_error();
}

Dies ist sehr schwer zu lesen und zu verstehen, weil der eigentliche Zweck der Funktion tief geschachtelt und sehr unübersichtlich versteckt ist. Geübte Programmierer verwenden stattdessen absichtlich die in der strukturierten Programmierung verpönten Schleifenkurzschlüsse und frühzeitigen Funktionsausstiege in einer bestimmten Form, um besser lesbaren Code zu schreiben:

if (!vorbedingung)
  handle_error();

if (!vorbedingung2)
  handle_error2();

if (!vorbedingung3)
  handle_error3();

doit; // Nutzlast

Dieser Code skaliert sich besser: Egal wieviele Vorbedingungen zu erfüllen sind - die Einrücktiefe bleibt konstant. Außerdem steht der normale Fall jetzt in Falllinie und der Fehlercode ist als Ausnahme eingerückt und zur Seite gedrängt.

Ein praktisches Beispiel: Der folgende Code zum Durchlesen eines Verzeichnisses

$d = dir("d:/logfiles");
while($entry=$d->read()) {
  if (($entry != ".") && ($entry != "..")) {
    doit();
  }
}
$d->close();

wird durch die Umstellung zu

$d = dir("d:/logfiles");
while ($entry = $d->read()) {

  if ($entry == "." or $entry == "..")
        continue;

  doit();
}
$d->close();

27.2. Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform

Antwort von Kristian Köhntopp

Da jede HTML-Datei auch ein gültiges, bedeutungsgleiches PHP-Programm ist, existiert ein einfacher und systematischer Weg, um von einem HTML-Formular zu einem PHP-Programm zu kommen, das dieses Formular bearbeitet. Hält man sich an diesen Weg, wird das resultierende Formular zugleich ein bestimmtes Format haben.

Der erste Schritt ist die Entwicklung eines reinen HTML-Formulares, das die zu verarbeitenden Daten abfrägt.

In einem zweiten Schritt wird man aus diesem Formular ein sogenanntes Affenformular machen, indem man dafür sorgt, dass das Formular sich selbst aufruft und seine alten Werte immer wieder einsetzt. Es heißt Affenformular, weil eine Million Affen dieses Formular eine Million mal aufrufen können, ohne etwas zu bewirken.

<form action="<?php echo $_SERVER['PHP_SELF']; ?>">
<input type="text"
       name="textfeld"
       value="<?php if (isset($_REQUEST['textfeld'])) echo htmlspecialchars($_REQUEST['textfeld']); ?>">
<br>
<input type="submit"
       name="do_form_x"
       value="Ausführen">
</form>

Das Affenformular enthält an zwei Stellen PHP-Code: Bei 'action' wird der Dateiname des Skripts eingetragen. Durch Einsetzen von $_SERVER['PHP_SELF'] (oder vor PHP 4.1.0 $HTTP_SERVER_VARS['PHP_SELF']) an Stelle des Dateinamens kann das Formular seine eigene Adresse bestimmen und sich somit selbst aufrufen. Und außerdem werden die Defaultwerte der verschiedenen Formularfelder durch PHP wieder mit den Ausgangswerten belegt. Wenn das Formular also abgesendet wird und die Eingabewerte nicht korrekt sind, wird das Formular wieder dargestellt und die alten Werte werden als Standardwerte wieder eingesetzt. Damit kann der Anwender sie korrigieren, ohne sie noch einmal eingeben zu müssen.

Der dritte Schritt besteht dann darin, eine Reihe von Funktionen zu codieren, die die Werte aus diesem Formular validieren (siehe Wie erkenne ich fehlerhafte/fehlende Eingaben?) und ggf. die passenden Fehlermeldungen erzeugen, die dann an den geeigneten Stellen wieder in das Formular eingesetzt werden. Das Endresultat muss dann eine Variable haben, an der entscheidbar ist, ob der Formularinhalt gültig ist oder nicht.

<?php

// Funktion zum Drucken von Fehlermeldungen
function errmsg($msg) {
   ?>
   <font color="#ff0000"><b><?php print nl2br($msg) ?></b></font>
   <?php
}

// Überprüft Eingabewerte für $textfeld auf Korrektheit.
function validate_textfeld($val) {
   $msg = "";
   if (strlen($val) < 3)
      $msg .= "Die Eingabe muss mindestens 3 Zeichen lang sein.\n";

   if (preg_match("/\s/", $val))
      $msg .= "Die Eingabe darf keine Leerzeichen "
             ."oder Tabulatoren enthalten.\n";

   return $msg;
}

// Für jedes Formularfeld werden nun ein oder mehrere
// Validatoren aufgerufen und das Ergebnis der Überprüfung
// gemerkt.
$valid = true;
if (isset($_REQUEST["textfeld"])) {
   $error["textfeld"] = validate_textfeld($_REQUEST["textfeld"]);
   if ($error["textfeld"] != "")
      $valid = false;
}

?>
<form action="<?php print $_SERVER["PHP_SELF"]; ?>">
<input type="text"
       name="textfeld"
       value="<?php print htmlspecialchars($_REQUEST["textfeld"]); ?>"><br>
<?php
// Ggf. Fehlermeldung ausdrucken.
if ($error["textfeld"] != "")
   print errmsg($error["textfeld"]);
?>
<input type="submit"
       name="do_form_x"
       value="Ausführen">
</form>
<hr>
<?php if ($valid and isset($_REQUEST["do_form_x"])) { ?>
<!-- Nutzlast -->
<?php } ?>

Schließlich kann man im vierten Schritt daran gehen, die validierten Formulardaten einer Bearbeitungsfunktion zu übergeben und diese Funktion die eigentliche Arbeit machen zu lassen. Das Ergebnis dieser Verarbeitung wird in den meisten Fällen unterhalb des Formulars dargestellt werden, so dass man mit den Eingabedaten oben gleich die nächste Abfrage starten kann.

An dieser Stelle hat man ein funktionierendes Formular und zwei Baustellen, an denen man weiterarbeiten kann: Zum einen muss man sich Gedanken darüber machen, welche Funktionalität aus diesem Formular an anderer Stelle so auch wieder verwendet werden könnte und sollte sich überlegen, ob man nicht eine Klasse schreiben möchte, in die man diese Funktionalität anwendungsunabhängig kapseln könnte. Auf diese Weise wird man sich Schritt für Schritt eine kleine Bibliothek an Funktionen zulegen, die in vielen anderen Projekten ebenfalls Anwendung finden kann.

Zum anderen muss man sich überlegen, ob man die Verkettung von Bildschirmen, Knöpfen und Aktionen nicht ein wenig globaler lösen kann und welche Struktur man seinem Programm dafür geben wird.

27.3. Trenne Aussehen und Inhalt

Antwort von Martin Jansen

Es ist immer günstig Aussehen und Inhalt zu trennen.

Die gängigsten Template-Systeme in PHP sind:

27.4. Was sind eigentlich if-Schleifen?

Keywords: if | Bedingung

Antwort von Johannes Frömter

Sogenannte "if-Schleifen" sind anscheinend nicht auszurottende Hirngespinste. Es gibt schlicht und einfach keine "if-Schleifen", weder in PHP noch in irgendeiner anderen Programmiersprache.

Es gibt if-Anweisungen, die einen Codeteil abhängig von einer Bedingung ausführen oder nicht, und es gibt for-, while- und do-while-Schleifen, die einen Codeteil mehrmals ausführen.

Schleifen und Bedingungen können selbstverständlich verschachtelt werden, d.h. in einer Schleife kann eine Bedingung abgefragt werden, oder abhängig von einer Bedingung kann eine Schleifenkonstruktion ausgeführt werden. Aber "if-Schleifen" erhält man deswegen trotzdem nicht.

27.5. Anführungzeichen oder Hochkomma?

Antwort von Johannes Frömter

In PHP können Strings sowohl von normalen Anführungszeichen (") als auch von Hochkommata (', auch genannt Apostroph, einfache Anführungszeichen, single quote, ASCII 39) eingefasst werden. Es gibt jedoch einen grundlegenden funktionalen Unterschied zwischen diesen beiden Zeichen: der Bereich zwischen Anführungszeichen wird von PHP ausgewertet, während alles, was zwischen Hochkommata steht, von PHP schnell übergangen wird.

Diese Tatsache kann man natürlich nutzen, da man weniger escapen muss (z.B. bei Windows-Pfadangaben, s.u.), außerdem werden Strings in Hochkommata schneller verarbeitet. Allerdings werden dann Variablen nicht durch ihren Wert ersetzt ("interpoliert"), aber das kann ja durchaus auch gewollt sein. Die einzigen Zeichenfolgen, die innerhalb von Hochkommata eine besondere Bedeutung haben, sind \\ (ergibt einen einzelnen Backslash) und \' (ergibt ein geschütztes Hochkomma). Beispiele:

echo 'Micro$oft';  // ergibt: Micro$oft
echo "Micro$oft";  // ergibt: Micro + Inhalt der Variable $oft
echo "Micro\$oft"; // ergibt: Micro$oft

echo 'c:\temp';    // ergibt: c:\temp
echo "c:\temp";    // ergibt: c: + Tabulator + emp
echo "c:\\temp";   // ergibt: c:\temp
echo 'c:\\temp';   // ergibt: c:\temp

echo 'Kein Hochkomma: \x27';  // ergibt: Kein Hochkomma: \x27
echo "Ein Hochkomma:  \x27";  // ergibt: Ein Hochkomma: '
echo 'Ein Hochkomma: \'';     // ergibt: Ein Hochkomma: '

echo "<input name='foo' value='$bar'>";  // gültiges HTML

Die Benutzung von Hochkomma statt Anführungszeichen wie im letzten Beispiel ist in HTML erlaubt (siehe die HTML 4.01 bzw. XML 1.0 Spezifikationen).

27.6. Mein Script funktioniert nicht mit Browser XY!

Antwort von Johannes Frömter

PHP-Scripte werden bekanntlich auf dem Server ausgeführt und liefern in der Regel schlichte HTML-Dokumente an den Browser. PHP selbst kann also nichts dafür, wenn ein Script mit einem bestimmten Browser nicht "funktionieren" sollte. Die Fehlerursache ist vielmehr im Script selbst zu suchen, denn dieses produziert sehr wahrscheinlich ungültigen HTML-Quellcode.

Ein beliebter Fehler ist z.B. falsche Groß-/Kleinschreibung - Variablen in PHP sind case-sensitive, d.h. es wird zwischen Groß- und Kleinschreibung unterschieden! Folgendes HTML-Formular ist dadurch fehlerhaft geworden. Je nachdem, wie tolerant sich die einzelnen Browser verhalten, kann das Formular trotzdem funktionieren, oder es funktioniert eben nicht.

// PHP-Script (Verwendung von $_server statt $_SERVER):
<form action="<?php echo $_server['PHP_SELF']; ?>" method="post">

// HTML-Ergebnis:
<form action="" method="post">  // action ist leer!

Am besten schickt man die Ausgabe des fraglichen Scriptes erst mal durch den W3C HTML Validation Service und merzt alle monierten Regelverstöße aus, bevor man Dritte mit dem (vermeintlichen) Problemscript behelligt.

Hilfe bei HTML-Problemen gibt es bei SELFHTML und in der Newsgroup de.comm.infosystems.www.authoring.misc. Browser-spezifische Dinge werden in der Gruppe de.comm.software.browser.misc diskutiert.

27.7. Schaden Kommentare der Performance?

Antwort von Richard Körber

Kommentare und Einrückungen schaden nicht der Performance!

PHP4 überspringt alle Kommentare und überflüssigen Leerzeichen (zum Beispiel Einrückungsleerzeichen) bereits beim Parsen des Programms. Das geht selbst bei extrem umfangreichen Kommentaren vernachlässigbar schnell.

Zur Laufzeit macht es für PHP dann keinen Unterschied mehr, ob im Quelltext Kommentare standen oder nicht. Selbst in Schleifen wirken sich Kommentare nicht nachteilig auf die Performance aus.

Der minimale Zeitverlust beim Programmstart ist kein Grund, auf eine gute Kommentierung zu verzichten. Wer sein Script performanter machen möchte, sollte sich eher darauf konzentrieren, Schleifen zu optimieren, bevorzugt Variablen als Referenz zu übergeben oder schnellere Algorithmen zu finden. Mit einem PHP-Profiler lassen sich besonders zeitintensive Programmabschnitte schnell ausfindig machen.

27.8. Wie kann ich das Caching einer Seite verhindern?

Keywords: Cache | Reload | Lebensdauer | Browser

Antwort von Kristian Köhntopp

In HTTP 1.0 kann man das Caching einer Seite nur unvollständig steuern. Die ersten Webcaches haben die Lebensdauer von Seiten im Cache auf der Grundlage des Erzeugungsdatums geschätzt oder, wenn ein Expires-Header angegeben, diesen beachtet. Später ist der spezielle Header Pragma: no-cache eingeführt worden, um das Caching von Seiten durch Webcaches und Browser zu verbieten.

Erst mit HTTP 1.1 kann eine spezielle Cache-Steuerung hinzu, die zwischen privaten (browsereigenen) Caches und öffentlichen Caches unterschied. Über den besonderen Header Cache-Control kann man die Lebensdauer von Seiten in Caches steuern.

Microsoft kocht zusätzlich noch eine Spezialsuppe, indem sie für den MSIE 5.x spezielle Cache-Control Extensions definieren.

Um das Caching einer Seite zu erlauben, kann man den folgenden Code verwenden:

$expire = 15;  // Lebensdauer der Seite im Cache in Minuten

$exp_gmt = gmdate("D, d M Y H:i:s", time() + $expire * 60) ." GMT";
$mod_gmt = gmdate("D, d M Y H:i:s", getlastmod()) ." GMT";

// HTTP 1.0
header("Expires: " . $exp_gmt);
header("Last-Modified: " . $mod_gmt);

// HTTP 1.1
header("Cache-Control: public, max-age=" . $expire * 60);

Um das Caching einer Seite auf private Caches zu begrenzen muss man Code wie diesen nehmen:

$expire = 15;  // Lebensdauer der Seite im Cache in Minuten

$mod_gmt = gmdate("D, d M Y H:i:s", getlastmod()) ." GMT";

// HTTP 1.0 kennt keine privaten Caches, also nix cachen
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . $mod_gmt);

// HTTP 1.1
header("Cache-Control: private, max-age=" . $expire * 60);

// MSIE 5.x special
header("Cache-Control: pre-check=" . $expire * 60, FALSE);

Um das Caching einer Seite zu verhindern, ist der folgende Code passend:

header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") ." GMT");
header("Cache-Control: no-cache");
header("Pragma: no-cache");
header("Cache-Control: post-check=0, pre-check=0", FALSE);

Ein anderer Trick, mit dem man das Caching einer Seite gut verhindern kann, ist das Anhängen von Parametern an die URL einer Seite in der Form http://www.meinserver.de/bla.php?x=y oder das Einfügen von benutzerspezifischen Komponenten in die URL (in den Hostnamen, den Pfad oder den Dateinamen).

27.9. Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?

Keywords: Redirect | Header | 302 | HTTP | URL

Antwort von Kristian Köhntopp

Um einen Redirect zu erzeugen, muss man den HTTP-Header Location senden und dort die neue URL angeben. Zum Senden von HTTP-Headerzeilen verwendet man die PHP-Funktion header() . Diese Funktion kann nur dann verwendet werden, wenn PHP noch keinen HTTP-Body ausgegeben hat, wenn also weder Fehlermeldungen, Leerzeilen, Leerzeichen noch HTML ausgegeben worden sind.

// Redirect-Ziel
header("Location: http://www.ziel.de/zielseite.html");

Wichtig: RFC 2616 schreibt im Abschnitt 14.30 Location eine sog. absoluteURI vor, d.h. die Adresse muss mit http:// beginnen, relative Anweisungen à la "Location: index.html" sind nicht standardkonform! Manche Browser sind zwar so tolerant, relative Angaben zu verstehen und in der Lage, selbständig die absolute Adresse zu ermitteln, aber verlassen kann man sich darauf nicht; die PHP-Funktion fopen() z.B. scheitert an derart ungültigen Location-Angaben.

27.10. Wie kann ich mit PHP WAP-Seiten erzeugen?

Keywords: WAP | WML | Header | Content-Type

Antwort von Richard Körber

Zuerst sollte man ein eigenes Verzeichnis für die WAP-Seiten anlegen und dem Apache beibringen, auch Dateien mit der typischen WAP-Endung .wml von PHP generieren zu lassen. Dies geschieht zum Beispiel mit folgender .htaccess-Datei in dem WAP-Verzeichnis:

AddType application/x-httpd-php .php .wml
DirectoryIndex index.wml

Schick ist, jetzt eine Subdomain wap auf dieses Verzeichnis verweisen zu lassen.

Es ist anschließend PHPs Aufgabe, mit der header() -Funktion einen korrekten Content-Type zu setzen. Ein Charset muss dabei angegeben werden! So könnte ein typisches WAP-PHP-Script beginnen:

<?php
  header("Content-Type: text/vnd.wap.wml;charset=iso-8859-1");
  print("\n\n<?xml version=\"1.0\"?>\n");
?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">

Darauf folgt der eigentliche WML-Content, in den man wie gewohnt PHP einbetten kann. Drei Dinge sind dabei wichtig:

  • WML ist eine XML-Sprache. Man muss penibelst darauf achten, dass der Code korrekt ist! Fehler in der Verschachtelung oder Schreibweise der Tags sind fatal.

    WAP lernt man zum Beispiel bei SELFWML oder bei WAP Basics.

  • Die von HTML bekannten Entities wie &ouml; funktionieren bei WAP nicht. Stattdessen verwendet man die ganz normalen Umlaute etc. Das kann unter Umständen recht ärgerlich werden, wenn man Funktionen geschrieben hat, die bereits solche Entities liefern. Laut WAP-FAQ sind aber auch Entities der Art &#246; erlaubt.

  • Handys haben recht wenig Speicherplatz. Die Seite sollte daher maximal 1400 Zeichen groß sein, damit alle Handys sie anzeigen können.

WAP erlaubt für Bilder ausschließlich das eigene Bildformat WBMP. Ein Konverter ist zum Beispiel im Paket von Deck-it enthalten. Bilder sollten sparsam verwendet werden und möglichst klein sein. Die Übertragung kostet Zeit, Geld und den sowieso schon knappen Speicherplatz auf dem Handy.

Cookies führen zwar zu keiner Fehlermeldung, aber man sollte sich nicht darauf verlassen, dass sie funktionieren!

Testen kann man die Seite mit WAP-Simulatoren, zum Beispiel Deck-it oder über verschiedene Online-Emulatoren. Letztendlich zählt aber nur eins: der Test mit dem echten Handy.

Antwort von Kerry W. Lothrop

Das Verarbeiten von WML-Formularen funktioniert mit PHP fast wie bei HTML-Formularen, mit dem kleinen Unterschied, dass manche Browser Formulardaten als Unicode versenden. Die Funktion utf8_decode() kann verwendet werden, um die Sonderzeichen zu dekodieren.

Seit GD 1.8 kann PHP auch WBMP-Grafiken erstellen. Die relevanten Funktionen heißen imagewbmp() , jpeg2wbmp() und png2wbmp() .

28. Häufige Fehlermeldungen

28.1. Was ist das für ein @-Zeichen vor einigen Funktionsaufrufen?

Antwort von Kristian Köhntopp

Manche PHP-Anweisungen können Fehlermeldungen, Warnungen oder Hinweise generieren. Diese Meldungen werden bei der Ausführung von Scripten Bestandteil der Ausgabe, wo sie unter Umständen sehr stören können. Stellt man einem Funktionsaufruf ein @-Zeichen voran, wird der Interpreter die Ausgabe der Meldung unterdrücken.

Wenn die Konfigurationsanweisung track_errors aktiv ist, werden die Meldungen stattdessen in der Variablen $php_errormsg hinterlegt.

Diese Eigenschaften von PHP sind im Kapitel Error Handling dokumentiert.

28.2. Warning: Supplied argument is not a valid File-Handle resource

Antwort von Kristian Köhntopp

Ein Script versucht mit einem Filehandle ($fp) zu arbeiten, welches das Resultat eines fopen() ist ($fp = fopen("..", "r") oder ähnlich). Dieses Filehandle ist ungültig, z. B. weil die Datei nicht existiert oder die Zugriffsrechte das Öffnen nicht gestatten.

Das Script ist fehlerhaft, weil es nach dem fopen() nicht prüft, ob das fopen() erfolgreich war:

        $fp = fopen(..., "r");

        /* Das fehlt zum korrekten Code:  */
        if (!$fp)
                die("Kann Datei ... nicht oeffnen.\n");

        /* Diese Anweisung macht dann Ärger */
        while($line = fgets($fp, 1000)) {
        ...
        }

28.3. Fatal error: Maximum execution time exceeded

Antwort von Kristian Köhntopp

Mit Hilfe des Parameters max_execution_time in der php.ini lässt sich die maximale Laufzeit eines PHP-Scriptes in Sekunden festlegen. Der Interpreter beendet sich selbst, wenn ein Script mehr als die dadurch zugewiesene Zeit läuft.

Wenn kein safe_mode aktiviert ist, kann ein Script sich selbst dieses Laufzeit-Limit mit Hilfe der Funktion set_time_limit() neu setzen.

28.4. Supplied argument is not a valid MySQL result...

Antwort von Kristian Köhntopp

Die Funktion mysql_query() liefert als Ergebnis 0 bzw. false, wenn die gesendete Query ungültig oder syntaktisch falsch ist. Es ist notwendig, diesen Fall abzufangen um die Meldung zu verhindern. Mit den Funktionen mysql_error() und mysql_errno() kann man auf die letzte Fehlermeldung bzw. die letzte Fehlernummer des MySQL-Servers zugreifen, wodurch sich die Fehlerursache meist leicht ermitteln lässt.

Siehe hierzu "Meine Datenbankabfrage funktioniert nicht".

28.5. Windows: Call to unsupported or undefined function: xy()

Antwort von Johannes Frömter

PHP für Windows ist modular aufgebaut, einige Funktionen (z. B. die IMAP -, die LDAP - oder die Grafik -Funktionen) sind standardmäßig nicht vorhanden. Die verschiedenen Module liegen als DLL-Dateien vor und müssen geladen werden. Dies kann teilweise zur Laufzeit mit Hilfe der Funktion dl() geschehen, oder aber durch die passenden Einträge im Abschnitt "Dynamic Extensions" in der php.ini (siehe auch: "Wo finde ich die php.ini?"):

;;;;;;;;;;;;;;;;;;;;;;
; Dynamic Extensions ;
;;;;;;;;;;;;;;;;;;;;;;
; directory in which the loadable extensions (modules) reside
extension_dir   =  d:\php4\extensions\

; if you wish to have an extension loaded automatically, use the
; following syntax:  extension=modulename.extension
extension=php_imap.dll

Wird PHP als Webserver-Modul eingesetzt, muss der Webserver neu gestartet werden, um die Änderungen in der php.ini aktiv werden zu lassen. Sowohl in der Distribution von php4win als auch in den Windows-Binaries von PHP.net sind zahlreiche Extensions enthalten.

28.6. Unix: Call to unsupported or undefined function: xy()

Antwort von Kristian Köhntopp

Höchstwahrscheinlich wurde das Modul bei der Installation von PHP nicht mitkompiliert ...

Ein Blick in den entsprechenden Abschnitt im PHP-Manual verrät oft schnell, welche Einstellung man beim Kompilieren beachten sollte.

Wenn ein Neukompilieren von PHP nicht möglich/erwünscht ist, kann man zur Laufzeit eine kompilierte shared PHP-Extensions (*.so) einbinden. Diese Extensions können mittels phpize erstellt werden:

# Beispiel: calendar-Extension
$ cd php4/ext/calendar
$ phpize
Configuring for:
  PHP Api Version:   20020918
  Zend Module Api No:   20020429
  Zend Extension Api No:   20021010
$ ./configure
...
$ make
...
Libraries have been installed in:
   /home/user/src/php4/ext/calendar/modules
...

Mittels dl() können diese Binaries dann in Scripten eingebunden werden.

28.7. Warning: xy() is not supported in this PHP build

Antwort von Johannes Frömter

Der Funktionsname ist PHP zwar bekannt (im Gegensatz zu "Windows: Call to unsupported or undefined function: xy()"), aber der Funktionsaufruf wird in dieser Version von PHP nicht unterstützt. Das kann z. B. bei neuen Funktionen der Fall sein, die noch nicht auf Windows portiert wurden oder die auf Windows-Systemen prinzipiell nicht anwendbar sind.

Es kann sich auch schlicht um ein Versehen beim Kompilieren handeln, dann sollte ein Blick in das Bug-System Klärung verschaffen. So wurde z. B. in PHP 4.0.5 für Windows die Funktion crypt() im Build nicht berücksichtigt. In allen Versionen davor und danach ist sie aber enthalten.

28.8. MySQL-Server has gone away

Keywords: SQL | Datenbank | MySQL | Fehler | gone

Antwort von Kristian Köhntopp

Mögliche Ursachen dafür werden in der MySQL-Dokumentation in Kapitel 18.2.1 diskutiert. Die Fehlermeldung kann durch einen Idle-Timeout auf der Datenbankverbindung (8 Stunden) oder durch zu große Datenpakete beim Arbeiten mit BLOBs und TEXT-Feldern auftreten. In letzterem Fall empfiehlt die MySQL-Dokumentation das Setzen der MySQL-Konfigurationsvariablen max_allowed_packet=# auf einen großen Wert (einige MB).

Ein Benutzer berichtete vom Auftreten dieses Fehlers beim Arbeiten mit UPDATE-Anweisungen in einer breiten Tabelle, die auch bis zu 150 KB große TEXT-Felder enthielt. Abhilfe war hier das Auslagern der TEXT-Felder in eine separate Tabelle. Dies brachte zugleich auch einen großen Performancegewinn.

28.9. Unix: Call to unsupported or undefined function: OCILogon()

Antwort von Anton Bangratz

Wenn die Konfiguration von PHP mit --with-oci8 korrekt erfolgt ist, dann kann mit einiger Sicherheit angenommen werden, dass ORACLE nicht korrekt installiert wurde. Falls man die Fehlermeldungen von configure und make überlesen hat, ist es möglich, dass der Oracle-Support nicht oder nur fehlerhaft in PHP einkompiliert wurde. Dies geschieht meist dann, wenn man übersieht, dass die Headerdateien, die von PHP benötigt werden, in der ORACLE Clientinstallation nicht enthalten sind. Die Dateien befinden sich in $ORACLE_HOME/rdbms/demo/ und $ORACLE_HOME/rdbms/public/.

Lösung: Nachinstallieren von Pro*C/C++ und der Demodateien aus der ORACLE-Server Distribution, danach Neukompilierung von PHP.

28.10. Warning: ORA-12154: TNS:could not resolve service name

Antwort von Anton Bangratz

Diese Meldung kann zwei Ursachen haben:

Der TNS/-Name, der bei der Verbindung zu einem ORACLE-Server benötigt und in OCILogon() übergeben wird ist falsch, oder der TNS/-Name ist in der Datei tnsnames.ora im Verzeichnis $ORACLE_HOME/network/admin/ nicht eingetragen.

Lösung: Der Eintrag in der Datei tnsnames.ora muss korrekt sein, was man mit tnsping oder SQL*Plus überprüfen kann. Dieser Eintrag muss dann genau so der Funktion OCILogon() übergeben werden.

Achtung: Kommt stattdessen die Meldung "oci_open_server: Error while trying to retrieve text for error ORA-12154", so ist die Umbgebungsvariable ORACLE_HOME falsch oder nicht gesetzt. Der nächste Abschnitt enthält weitere Informationen zu Umgebungsvariablen.

28.11. Warning: ORA-12705: invalid or unknown NLS parameter value specified

Antwort von Anton Bangratz

Die in Umlaute, die in die Datenbank eingetragen wurden, werden nicht korrekt dargestellt. erwähnten Einstellungen sind nicht korrekt. Der Datenbankserver versteht entweder das Character Set nicht oder der Pfad in ORA_NLS33 ist nicht korrekt.

28.12. Warning: Cannot send session cookie - headers already sent ...

Keywords: Cookie | Header | Cache | Fehler | Session | HTTP

Antwort von Daniel T. Gorski

Wie bei allen anderen HTTP-Headern, darf auch vor dem Setzen von Cookies (Sessionfunktionen von PHP4 benutzen standardmäßig Cookies) kein einziges Byte des Codes an den Client gesendet werden. Erst wenn alle HTTP-Header gesendet worden sind, dürfen entsprechende Daten (z. B. HTML) gesendet werden.

Typische Stolperfallen sind Leerzeichen bzw. -zeilen vor dem ersten <? bzw. <?php Delimiter des Scripts oder mit include() oder require() importierte Scriptfragmente, die natürlich gar keine Ausgabe produzieren dürfen - weder vor dem ersten Delimiter, noch dazwischen, noch nach dem letzten.

Zusätzlich kommt durch die auto_prepend_file-Einstellung in der php.ini, bzw. in der Webserverkonfiguration, eine weitere potentielle Fehlerquelle hinzu.

28.13. Warning: Cannot add header information - headers already sent ...

Keywords: Fehler | Header | HTTP | HTML | Cookie | Weiterleitung

Antwort von Johannes Frömter

Aufrufe von header() müssen vor allen anderen Ausgaben an den Client erfolgen. Es spielt keine Rolle, ob die Ausgabe durch einen HTML-Bereich vor dem Script oder durch das PHP-Script selbst entsteht. Dabei reicht als "HTML-Bereich" z. B. bereits ein einziges Leerzeichen vor dem öffnenden PHP-Tag (" <?php").

Siehe auch "Warning: Cannot send session cookie - headers already sent ...".

28.14. Warning: Wrong parameter count for xy()

Antwort von Johannes Frömter

Die Funktion xy() erwartet eine andere Anzahl an Parametern, als ihr im Aufruf mitgegeben wurden. So erwartet z. B. die Funktion date() einen und optional einen zweiten Parameter, da sie wie folgt definiert ist: date (string format [, int timestamp]). Parameter in eckigen Klammern sind optional und können von rechts nach links weggelassen werden. D. h. sowohl date() (kein Parameter) als auch date('H:i', 1005698576, 15) (drei Parameter) erzeugen eine Warnung, weil die Zahl der Argumente nicht mit der Funktionsdefinition übereinstimmt. Gültig sind dagegen date('H:i') (ohne Timestamp) und date('H:i', 1005698576) (mit Timestamp).

Selbst definierte Funktionen reagieren nur allergisch, wenn sie mit weniger Parametern aufgerufen werden, als in der Funktionsdefinition vorgesehen. Optionale Parameter müssen mit einem Vorgabewert versehen werden, damit das funktioniert: function xy($var = "default") (Vorgabewerte sind nur bei Call-by-Value, nicht bei Call-by-Reference möglich).

28.15. Notice: Use of undefined constant ...

Antwort von Johannes Frömter

Es wird eine Konstante verwendet, obwohl diese nicht definiert ist. Aus purer Freundlichkeit nimmt PHP den Namen der Konstante (!) als eigentlich gemeinten Wert an, belässt es bei einem Hinweis und lässt das Script weiterlaufen. Häufig wird dieser Fehler beim Zugriff auf Arrays gemacht, indem der Schlüssel direkt in eckigen Klammern steht, statt korrekt in Anführungszeichen eingefasst zu werden:

$array[key]     // Falsch, wenn key keine Konstante ist
$array["key"]   // So war es gemeint
$array['key']   // Besser, da nichts ausgewertet werden muss
$array[12]      // Richtig, Zahlen als Key ohne Anführungszeichen

$array[$key]    // Ok: Zugriff per Variable
$array["$key"]  // Hier sind die Anführungszeichen unnötig
$array['$key']  // Falsch, Variable wird nicht ausgewertet

"$array[key]"   // Korrekt (!), da innerhalb eines Strings
"$array[$key]"  // Korrekt
"$array['key']" // Parse error!

In neueren PHP-Versionen ist der Error reporting level defaultmäßig so eingestellt, dass eine Warnung ausgegeben wird. Das Script sollte unbedingt nachgebessert werden. Es ist zwar auch möglich, PHP auf einen niedrigeren Error reporting level zu konfigurieren (error_reporting = E_ALL & ~E_NOTICE;), aber das ist auf Dauer nicht empfehlenswert, da es nur die Ausgabe der Warnung unterdrückt, nicht aber die eigentliche Ursache des Problems beseitigt (außerdem dauert der Arrayzugriff ca. 4-5 mal länger!).

28.16. Notice: Undefined variable ...

Antwort von Johannes Frömter

Ursachen

Möglicherweise erscheint diese Meldung bei Scripten, die bisher unter älteren PHP-Versionen liefen und jetzt unter PHP 4.1.0 oder neuer eingesetzt werden. Grund dafür ist häufig das ab dieser Version defaultmäßig strenger eingestellte error_reporting in der php.ini.

Der Hinweis wird ausgegeben, weil mit einer nicht definierten Variablen hantiert wird, d. h. der Name der Variablen existiert nicht, ein konkreter Wert ist folglich auch nicht verfügbar. Durch die automatische Typ-Konvertierung wird der angenommene "Wert" NULL dann zwar in einen passenden Datentyp verwandelt - das ist aber nur ein "Notbehelf", damit das Script weiterlaufen kann.

Wenn PHP mit register_globals = off konfiguriert ist, dann werden Variablen aus Formularen (oder aus dem Query-String im URL) nicht automatisch als $variable verfügbar gemacht. Diese Trennung zwischen Script-Variablen und Input-Variablen hat den Vorteil, dass nicht durch Aufruf von z. B. script.php?login=true die Script-interne Variable $login überschrieben wird.

Abhilfen

Handelt es sich um Variablen, die per GET oder POST übergeben werden, und PHP läuft mit der Einstellung register_globals = off, dann ist es aus Sicherheitsgründen zu empfehlen, das Script an die neuen Variablennamen anzupassen ($HTTP_GET_VARS/$HTTP_POST_VARS bzw. $_GET/$_POST, siehe: Release Notes zu PHP 4.1.0).

Handelt es sich um Script-interne Variablen, sollten diese vor der Verwendung definiert werden. Bevor man also $foo .= "bla"; schreibt, um an $foo etwas anzuhängen, ist ein $foo = ""; nötig, um die Variable zu definieren und ihr einen leeren String zuzuweisen.

Möchte man optionale Variablen (z. B. Checkboxen aus einem Formular) abprüfen, sollte man isset() verwenden, um auf die Existenz der Variablen zu testen:

// Nicht empfohlene Schreibweisen
if ($checkbox)
if ($var == 'wert')

// Empfohlene Schreibweisen
if (isset($_POST['checkbox']))
if (isset($_POST['var']) && $_POST['var'] == 'wert')

Es ist zwar auch möglich, PHP auf einen niedrigeren Error reporting level zu konfigurieren (error_reporting = E_ALL & ~E_NOTICE;), aber das ist auf Dauer nicht empfehlenswert, da es nur die Ausgabe der Warnung unterdrückt, nicht aber die eigentliche Ursache des Problems beseitigt.

28.17. Warning: Failed opening 'xy' for inclusion ...

Keywords: Fehler | include | Datei | URL | Windows

Antwort von Johannes Frömter

Dieser Fehler kann mehrere Ursachen haben:

  • Die Datei xy existiert nicht (möglicherweise ein Pfad-Problem)

  • xy ist eine lokale Datei, an die Parameter angehängt wurden - siehe hierzu: "Wie übergebe ich Variablen an eingebundene Dateien?"

  • PHP kann die Datei xy nicht lesen, weil es nicht die erforderlichen Rechte für die Datei hat (der Fehler tritt dann angeblich in Zeile 0 auf)

  • xy ist ein URL, allow_url_fopen steht aber auf 0 (siehe phpinfo() )

  • xy ist ein URL und wird mit include() unter Windows benutzt; das wird zumindest derzeit nicht unterstützt

All diese Probleme können analog auch mit require() auftreten, nur dass die Fehlermeldung dann Fatal error: Failed opening required 'xy' ... lautet.

28.18. Parse error: parse error in ...

Keywords: Fehler | Code | Syntax | Parser | Zeile | Semikolon | Klammer

Antwort von Johannes Frömter

Der PHP-Code enthält (mindestens einen) Syntax-Fehler. Der PHP-Parser kann den Code nicht interpretieren und stellt die Arbeit ein.

Die in der Fehlermeldung angegebene Zeilennummer muss dabei nicht unbedingt exakt die Zeile sein, in der sich der Fehler befindet; vielmehr kann der logische Fehler auch eine oder mehrere Zielen davor passiert sein, in der angegebenen Zeile tritt jedoch der syntaktische Fehler auf. Beispiel:

<?php
echo "H;    // Anführungszeichen vergessen
dies();
das();
?>          <-- hier tritt der Fehler auf!

Häufig sind vergessene Anführungszeichen oder nicht richtig geschlossene runde oder geschweifte Klammern die Ursache für Parse errors. Man sollte diese Zeichen immer paarweise schreiben und erst dann mit Inhalt füllen (also echo "" und dann zwischen die Anführungszeichen schreiben) und/oder einen Editor verwenden, der durch farbliche Markierung auf solche Unstimmigkeiten hinweist. Ein beliebter Fehler ist auch ein vergessenes Semikolon am Zeilenende.

28.19. 0 is not a MySQL result index

Antwort von Johannes Frömter

Siehe Supplied argument is not a valid MySQL result....

28.20. Warning: Unknown error ...

Keywords: Fehler | unbekannt | Mail

Antwort von Johannes Frömter

Tja, ein "unbekannter Fehler". Dieser Fehler kann vermutlich mehrere Ursachen haben, ein bekannter Fall ist die Verwendung der Funktion mail() ohne installierten (korrekt konfigurierten) Mailserver. Siehe hierzu auch: "Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?".

28.21. Warning: Failed to Connect ...

Keywords: Fehler | Verbindung | Mail

Antwort von Johannes Frömter

Vermutlich wurde die Funktion mail() ohne installierten (korrekt konfigurierten) Mailserver aufgerufen. Siehe hierzu auch: "Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?".

28.22. Warning: open(/tmp\sess_..., O_RDWR) failed ...

Keywords: Session | Windows | Pfad | Fehler | Datei | Pfad

Antwort von Johannes Frömter

Der Pfad für die Speicherung der Session-Daten lautet defaultmäßig /tmp, unter Windows muss der Pfad aber mit einem Laufwerksbuchstaben angegeben werden. Siehe "Wie benutze ich die Session-Funktionen unter Windows?".

28.23. Warning: fopen() - No such file or directory

Keywords: Fehler | fopen | Datei | URL

Antwort von Kai Schröder

Diese Warnung besagt, dass die Datei oder das Verzeichnis, das mit fopen() geöffnet werden soll, nicht existiert.

Die Ursache dafür kann vielfältig sein:

  1. Die Datei, die geöffnet werden soll, existiert nicht.

    Dies passiert besonders häufig beim Zugriff auf URLs. Prüfen lässt sich die Existenz mit einem Browser. Der häufigste Fehler ist die Verwendung von relativen URLs. Richtig sind absolute URLs, die mit http:// oder ftp:// beginnen.

    Quellcode-Beispiele:

    <?php
    /*
    Die externe URL http://www.example.com/verzeichnis/datei.ext soll gelesen werden.
    */
    
    /*
    in diesem Fall falsch, da eine URL absolut angegeben werden muss
    dieses Beispiel würde versuchen, die Datei datei.ext in
    /verzeichnis auf dem eigenen Server zu öffnen.
    */
    $fp = fopen("/verzeichnis/datei.ext", "r");
    
    /* richtig */
    $fp = fopen("http://www.example.com/verzeichnis/datei.ext", "r");
    ?>
    
  2. Das Verzeichnis existiert nicht.

    fopen() erstellt keine Verzeichnisse, wenn diese fehlen. Im Gegensatz dazu werden fehlende Dateien von fopen() automatisch angelegt, wenn sie zum schreiben geöffnet werden.

    Häufig wird in ChangeRoot-Umgebungen gearbeitet. Dies hat zur Folge, dass das Root-Verzeichnis des Users (z. B. /home/user/) von der Server-Root abweicht (/). Genaueres erfährt man durch die Verwendung von phpinfo() .

    Quellcode-Beispiele:

    <?php
    /*
    die DocumentRoot sei /home/user und die zu öffnende Datei file.ext
    im Verzeichnis mydir
    */
    
    /* falsch */
    $fp = fopen("/mydir/datei.ext", "r");
    
    /* richtig */
    $fp = fopen("/home/user/mydir/file.ext", "r");
    ?>
    

    Desweiteren kann es vorkommen, dass sich Tippfehler im Pfad befinden. Ein häufig übersehener Fehler sind Leerzeichen am Anfang, im Pfad oder an dessen Ende.

    Code-Beispiele:

    <?php
    /* falsch, Leerzeichen am Anfang */
    $fp = fopen(" /verzeichnis/datei.ext", "w");
    
    /* falsch, Leerzeichen im Pfad */
    $fp = fopen("/verzeichnis/ datei.ext", "a+");
    
    /* falsch, Leerzeichen am Ende */
    $fp = fopen("/verzeichnis/datei.ext ", "r+");
    
    /* richtig */
    $fp = fopen("/verzeichnis/datei.ext", "w");
    ?>
    

28.24. Warning: fopen() - No error

Keywords: Fehler | fopen | Success | URL

Antwort von Johannes Frömter

Es kann unter bestimmten Umständen vorkommen, dass PHP im Zusammenhang mit dem Befehl fopen() die Warnung "No error" oder "Success" ausgibt, was sich natürlich paradox anhört. Der Grund ist, dass beim Ausführen von fopen() zwar ein Problem auftrat, aber keine näheren Informationen über die Art des Problems vorliegen. Meist handelt es sich dabei um folgende HTTP-Status-Codes, die fopen() nicht verarbeiten kann:

  • Die angeforderte Ressource liegt in einem mittels HTTP Basic Authentication geschützten Bereich (Status-Code 401)

  • Es erfolgt ein nicht RFC-konformer Redirect. Korrekte Redirects mit absoluter URL kann fopen() jedoch handhaben (Status-Code 301 oder 302)

  • Die Ressource ist nicht vorhanden (Status-Code 404)

  • Der Zugriff ist verboten (Status-Code 403)

28.25. Document contains no data

Keywords: leer | Seite | Dokument | Daten | Browser | Fehler

Antwort von Kristian Köhntopp

Dies ist eine Netscape-Fehlermeldung, die dann auftritt, wenn der Webserver als Antwort auf einen Request genau Null Bytes Antwort liefert. Wenn dies geschieht und die Seite eigentlich eine Ausgabe erzeugen sollte, dann hat möglicherweise der PHP-Interpreter ein Problem und stürzt mit einem Coredump ab.

Der Microsoft Internet Explorer hat die Eigenart, in der Quelltext-Ansicht statt einer leeren Seite ein HTML-Grundgerüst zu präsentieren, das nicht vom Server stammt, sondern vom IE generiert wurde.

Handelt es sich um ein PHP-Problem, sollte man versuchen, den Fehler zu reproduzieren und das kürzestmögliche (!) Script, das den Fehler erzeugt, in den Bugreport mit einbinden.

28.26. Parse error on line 1 ... (bei Verwendung von XML/XHTML)

Antwort von Clemens Koppensteiner

Im Normalfall erlaubt PHP den Skriptbereicht mit <? ?> (den sogenannten Short-Tags) einzugrenzen, da diese Variante schneller zu schreiben ist - als <?php ?> - und man außerdem einfach Variablen ausgeben kann (<?=$variable ?>). Für PHP ist es aber unmöglich diese Notation von einer XML-Deklaration (<?xml version="1.0" ?>) zu unterscheiden; der Parser glaubt daher, dass es sich hierbei um PHP-Code handelt, der natürlich syntaktisch nicht korrekt ist.

Um das Parsen der Short-Tags abzuschalten, muss man den Eintrag short_open_tag in der php.ini auf off setzen. Danach wird nur mehr Code innerhalb der XML-konformen Processing Instructions (PI) <?php ?> bzw. dem <script language="php"> ... </script> Tag interpretiert.

Alternativ kann man natürlich auch die XML-Deklaration mittels echo ausgeben:

<?php echo '<?xml version="1.0" ?>'; ?>

28.27. Fatal error: Cannot redeclare class ... in ... on line ...

Antwort von Uwe E. Schirm

In den meisten Fällen werden immer wieder verwendete Funktionen und Klassen in seperate Dateien geschrieben und diese dann über include oder require in das Programm eingebunden. Wird über include oder require eine solche Datei zweimal geladen, wird der darin enthaltene Code auch zweimal ausgeführt. Das aber führt auf Grund der Redefinition der Funktion(en) und/oder Klasse(n) zu einem Fehler. Um diesen Fehler zu vermeiden, muss include_once() oder require_once() verwendet werden. Include_once und require_once verhalten sich beim ersten Aufruf wie include und require, bei wiederholtem Aufruf wird jedoch ein erneutes Laden der gleichen Datei verhindert.

28.28. Fatal error: Cannot redeclare ... (previously declared in ...) in ... on line ...

Antwort von Uwe E. Schirm

Siehe Fatal error: Cannot redeclare class ... in ... on line ....

29. Sessions

29.1. Wie realisiere ich Sessions mit PHP?

Antwort von Daniel T. Gorski

Sessiondaten sind Daten, die z.B. von einem PHP-Script auf dem Server gespeichert werden, und die meistens für die Dauer des Besuchs (Session/Sitzung) auf mehreren Pages einer Website ihre Gültigkeit behalten. Sessiondaten können z.B. dazu benutzt werden, um dem Besucher restriktiven Zugriff (z.B. nach einem Login) auf Teile einer Website zu erlauben - üblicherweise zu einem Administrationsbereich.

In der PHP4-Standardeinstellung werden diese Daten als ASCII- Dateien im /tmp-Verzeichnis (session.save_path) des Servers gespeichert. Diese Dateien enthalten das serialsierte ( serialize() ) Abbild der von einem Script gespeicherten Variablen. Der Name dieser Dateien setzt sich aus dem Prefix sess_ und einer 32-Zeichen langen, zufällig ausgewählten Zeichenkette (der Session-ID) zusammen.

Die Manipulation dieser Daten bleibt nur dem serverseitig ausgeführtem PHP-Script vorbehalten - der Client (Browser) weiß nicht welche Daten von dem Script gespeichert werden und er kann keinen mittelbaren Einfluss auf diese Daten nehmen. Ausnahme bildet hier die Änderung/Löschung dieser Daten durch den Administrator oder durch ein Script mit entsprechenden Rechten (siehe auch Wie schütze ich Sessiondaten zusätzlich?).

Unter diesen Voraussetzungen gelten die Sessiondaten bei einem korrekt installierten Webserver, als sicher und durch den Besucher nicht fakebar.

PHP4 stellt mehrere Funktionen zu Sessionverwaltung zu Verfügung (siehe auch die Sessionsreferenz , die grundlegenden seien hier kurz vorgestellt:

Bei Verwendung dieser Funktionen ist zu beachten, dass eine neue Session nur dann angelegt werden kann, wenn noch keine Ausgabe stattgefunden hat. Mehr Informationen dazu finden sich im Artikel Warning: Cannot send session cookie - headers already sent ....

session_start()

Session(datei) auf dem Server erstellen. Wurde eine gültige Session-ID übergeben, werden die in den Sessiondaten gespeicherten Werte in dem $_SESSION-Hash wiederhergestellt und abhängig von der Einstellung der register_globals auch als Variablen reinitialisiert.

Zusätzlich werden, je nach Einstellung des session.gc_probability -Parameters, in diesem Augenblick die angehäuften und nicht mehr benötigten Sessiondateien vom Server gelöscht. Wenn das error_reporting() schärfer eingestellt ist, gibt session_start() eine entsprechende Notice aus. Da diese üblicherweise nicht erwünscht ist, empfiehlt es sich vor die session_start() -Funktion ein "@" zu schreiben, welches diese Warnung unterdrückt.

Also:

<?php
    @session_start();
?>

Das $_SESSION-Hash

Der Zugriff auf Sessionvariablen erfolgt seit PHP 4.1.0 über das assoziative Array $_SESSION. Durch das erste Ansprechen eines Arrayelements wird die Sessionvariable angelegt, durch ein unset() kann eine Variable gelöscht werden. Bis PHP 4.0.6 hieß das entsprechende Array $HTTP_SESSION_VARS. Wenn register_globals aktiviert ist, müssen bei PHP Versionen, die älter als PHP 4.3 sind, die Sessionvariablen mit session_unregister() aus der Session entfernt werden.

session_destroy()

Die Funktion session_destroy() veranlasst alle Variablen einer Session zu verwerfen und die Session(datei) löschen.

Ein genauerer Einblick in die Funktionsweise dieser Funktionen wird hier gewährt: Was geschieht im Filesystem des Servers, wenn ich Sessions benutze?.

Beispiel 1

Beispiel 1: seite1.php

<?php
    // Falls nicht vorhanden, generiert eine Session(datei)
    // auf dem Server.
    // Falls bereits vorhanden, liest Sessiondaten wieder ein und
    // (re-)initialisiert die gespeicherten Variablen.

    @session_start();

    // Erst wenn das Script terminiert(!), merkt PHP den Inhalt
    // der Variablen $s_userName und $s_userPermissions in
    // den Sessiondaten.

    $_SESSION['userName']        =   "dtg";
    $_SESSION['userPermissions'] =   "keine :=(";
?>

Beispiel 1: seite2.php

<?php
    // Falls nicht vorhanden, generiert eine Session(datei)
    // auf dem Server.
    // Falls bereits vorhanden, liest Sessiondaten wieder ein und
    // (re-)initialisiert die gespeicherten Variablen.

    @session_start();

    // Gibt den Inhalt der wiederhergestellten Variablen aus.
    echo "<P>Hallo " . $_SESSION['userName'] . ",";
    echo "<P>Du hast " . $_SESSION['userPermissions'] . " Zugriffsrechte.";

    // Wird in diesem Script kein unset() oder
    // session_destroy() ausgeführt, bleiben die Daten erhalten!
?>

Damit das Folgescript (seite2.php) weiß, in welcher Datei auf dem Server die benötigten Daten zu finden sind, muss ihm die sog. Session-ID übergeben werden. Diese Session-ID repräsentiert den eindeutigen Namen der Session (es ist der bereits erwähnte 32-Zeichen lange String).

Um die Session-ID an eine andere Seite zu übergeben, benutzt PHP4 in der Standardeinstellung Session-Cookies, welche nicht gespeichert werden und mit dem Schließen des Browsers verfallen. In diesen Cookies wird der Name der Session (z.B. PHPSESSID) und die zugehörige Session-ID abgelegt. Bei einem Request auf ein Folgescript wird der Cookieinhalt mitgeschickt, und PHP weiß damit in welcher Sessiondatei die benötigten Daten gespeichert sind.

Dies passiert automatisch, ohne dass zusätzlicher Code geschrieben werden muss.

Beispiel 2

Hat man z.B. eine komplizierte Suchmaske z.B für eine Datenbank, aus der man eine noch kompliziertere Query basteln muss, über deren Ergebnisse man auf Folgeseiten browsen will, kann es sich lohnen die Grund-Query einmalig zu generieren, und sie dann weiter an das Ausgabe-/Browse-Script zu übergeben, anstatt alle Eckparameter immerwieder zu übergeben und die Query jedesmal neu zu generieren.

Dies darf natürlich nicht über die GET- oder POST-Methode geschehen, denn der User könnte durch die Manipulation dieser Parameter ohne Probleme "böse" Queries an die Datenbank schicken. Ein Datenverlust ist dann vorprogrammiert.

Für diesen Einsatzzweck eignen sich Sessions auch, denn die dynamisch erzeugte Query wird außerhalb der Reichweite des Users auf dem Server gespeichert.

Beispiel 2: seite3.php (Formularseite)

<?php
    @session_start();

    if (isset($_POST['submit']))
        {
            // Hier prüfen, ob _alle_ benötigten Variablen,
            // die an dieses Script übermittelt wurden,
            // legal sind!
            // Hier nur fortfahren, wenn alles in Ordnung ist

            // Die Query wird anhand der Variablen,
            // die submitted wurden, erzeugt
            $_SESSION['meineTolleQuery'] = "SELECT ...";

            // HTTP-Redirect zu der Ausgabeseite
            header("Location: http://" . $_SERVER['SERVER_NAME']
                                       . "/seite4.php");
            exit;
        }
?>

    <FORM action="<?php=$_SERVER['PHP_SELF']?>" method="post">
        <!--
            Hier ganz viele Radiobuttons, Checkboxen und Inputfelder
        -->
       <INPUT type="submit" name="submit">
    </FORM>

Beispiel 2: seite4.php (Ausgabeseite)

<?php
    @session_start();

    echo "<P>Diese Query wurde mir übermittelt: "
             . $_SESSION['meineTolleQuery'];
    echo "<P>und jetzt mache ich was draus!";
?>

29.2. Was ist eine Session-ID? Was ist PHPSESSID?

Keywords: Session-ID | SessionID | SID | PHPSESSID | GET | Session | Name | ID

Antwort von Daniel T. Gorski

Die sog. Session-ID ist ein zufällig ausgewählter Schlüssel, der die Sessiondaten auf dem Server eindeutig identifiziert. Dieser Schlüssel kann z.B. über Cookies oder als Bestandteil der URL an ein Folgescript übergeben werden, damit dieses die Sessiondaten auf dem Server wiederfinden kann.

PHPSESSID ist bei PHP4 der Default-Name der Session. Möchte man diesen Namen aus z.B. ästhetischen Gründen modifizieren - vor allem, wenn er als GET-Parameter als Teil der URL sichbar wird - so kann man dies in der php.ini, der Webserverkonfiguration oder direkt mit PHP bewerkstelligen. Der Artikel Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen? geht genauer darauf ein.

Eine Beispiel, wenn die Session-ID als GET-Parameter in der URL übertragen wird:

                            Query-String (alle GET-Parameter)
Protokoll     Subdomain      _________________|__________________
  |              |          /                                    \
http://www.daniel-gorski.de?query=xyz&PHPSESSID=cd45a3f73493d5d...
        |                |            \__________________________/
   Subdomain       Top Level Domain                |
                                       Session-Name und Session-ID

29.3. Wie stelle ich fest, ob der Client die Cookie-Annahme verweigert?

Antwort von Daniel T. Gorski

Am Beispiel der Session-Fallback-Klasse in Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback? kann man nachvollziehen, wie man feststellen kann, ob der Client Cookies akzeptiert oder nicht. Das Prinzip ist simpel: Das aufgerufene Script prüft, ob ihm eine Session-ID übergeben wurde. Falls nicht, forciert das Script einen Request auf sich selbst und übergibt dabei eine frisch erzeugte Session-ID als GET- Parameter an sich selbst. Beim zweiten Durchlauf des Scriptes kann dieses feststellen, ob ihm vom Browser ein Cookie mit der Session-ID übermittelt worden ist, oder ob es die Session-ID aus dem GET-Parameter benutzen soll.

29.4. Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback?

Antwort von Daniel T. Gorski

Viele Browser-User haben in ihrem Browser die Cookies aus diversen Gründen deaktiviert, oder lassen sich jeden Cookie bestätigen. Sollte ein Cookie auf dem lokalen Rechnersystem nicht gesetzt werden können, muss ein Ersatzmechanismus her - ein sog. Fallback. In diesem Fall ist der Fallback (Rückfall / Atavismus) eine Ersatzmethode, die die Übergabe der Session-ID an ein Folgescript ohne Cookies erlaubt.

Man kann den Fallback dadurch realisieren, dass man dem Folgescript die Session-ID quasi "manuell" bei jedem GET

<A href="test.session.php?<?php=SID?>">

oder POST - bei Formularen - mittels

<INPUT type  = "hidden"
       name  = "<?php=session_name()?>"
       value = "<?php=session_id()?>">

übergibt. ACHTUNG: Das bedeutet, dass jeder Link und jedes Formular innerhalb einer Website mit einem zusätzlichen Parameter versehen werden muss! Wird die Session-ID nicht korrekt übergeben, wird ein Folge-Script nicht auf die Sessiondaten zugreifen können - diese Daten sind für dieses Script "verloren".

PHP4 kann man auch mit dem --enable-trans-sid-Parameter kompilieren. Dann hat dies zufolge, dass, wenn der Client (Browser) keine Cookies annehmen kann/will, alle relativen Links einer Webpage mit dem zusätzlichen SessionName=Session-ID Parameter ergänzt werden. Dies klingt zunächst gut, aber man sollte beachten, dass durch den zusätzlichen Aufwand, den der PHP-Parser leisten muss, sich diese Technik nicht für High-Traffic-Websites oder Server mit vielen Vhosts eignet. Auch wenn man Projekte realisiert, die auf anderen Webservern laufen sollen, kann man nicht davon ausgehen, dass das betreffende PHP mit --enable-trans-sid kompiliert worden ist.

Die --enable-trans-sid-Technik versagt (PHP 4.0.1.pl2) auch bei folgenden Konstrukten:

<FORM action="<?php=$_SERVER['PHP_SELF']?>">

weil offensichtlich zuerst an den action-Tag die Session-ID-Information angehängt wird, und dann erst der Wert von $_SERVER['PHP_SELF']. Das Ergebnis sieht dann fälschlicherweise so aus:

<FORM action="?PHPSESSID=cd45a3f76f7325099c755b25b/test.session.php">

Fallback-Klasse

Wie bereits erwähnt, muss man u.U. selbst dafür Sorge tragen, dass der Fallback-Mechnismus greift, wenn der Client keine Cookies annimmt. Eine PHP-Klasse die das leisten kann ist auf http://develnet.org/ verfügbar.

Sie fügt bei Bedarf die Session-ID an Hyperlinks, Formulare und Redirects und ermöglicht die punktgenaue Steuerung des Fallbacks. Mit dieser Klasse muss man sich ebenfalls keine Gedanken beim Hosterwechsel machen, da man nicht auf die u.U. nicht vorhandene --enable-trans-sid-Funktionalität angewiesen ist.

Diese Klasse erlaubt zusätzlich das bequeme Registrieren von Session-Variablen, auch wenn PHP mit register_globals=off konfiguriert ist.

29.5. Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen?

Keywords: Session | Name | php.ini | aendern | umbenennen

Antwort von Daniel T. Gorski

In der php.ini wird der Name der Session in dem Parameter session.name festgelegt - standardmäßig auf PHPSESSID. Möchte man ohne Eingriff in die php.ini oder in die Webserverkonfiguration diesen Namen ändern, steht die Funktion session_name() zu Verfügung. Diese Funktion muss vor dem (Re)initialisieren der Sessiondaten ( session_start() ) ausgeführt werden.

<?php
    // Einen anderen Namen für die Session festlegen
    session_name("meineSession");
    @session_start();
?>

29.6. Wie schütze ich Sessiondaten zusätzlich?

Keywords: Session | Schutz | Host | virtuell | Sicherheit

Antwort von Daniel T. Gorski

Laufen auf einer Maschine mehrere virtuelle Hosts (vhosts), z.B. bei einem Hoster, muss der verantwortliche Administrator dafür Sorge tragen, dass - wenn überhaupt - der Safe-Mode nur für 100% vertrauenswürdige vhost-Nutzer ausgeschaltet werden darf ( safe_mode =Off). Dies würde - neben anderen systemnahen Eingriffen - erlauben, dass ein Script von einem anderen Vhost folgenden Code ausführen darf:

<?php
    // Inhalt von /tmp löschen
    system("rm -rf /tmp");
?>

Dies hätte in der PHP4-Standardeinstellung (aber mit safe_mode =Off) zufolge, dass alle Sessiondaten anderer Benutzer gelöscht worden wären. Gegebenfalls sollte man sich mit einer entsprechenden Anfrage an seinen Hoster wenden.

Eine andere Möglichkeit dies zu verhindern wäre es, für jeden Vhost eine andere Einstellung des session.save_path -Parameters zu wählen, wenn Sessiondaten in Files gespeichert werden.

29.7. Wie groß darf die Menge an Daten sein, die ich in einer Session speichern darf?

Antwort von Daniel T. Gorski

Diese Frage lässt sich im Grunde nur mit "maßvoll" beantworten. Der Unterschied zwischen wenigen Bytes und 5KB, wird sich nicht ungünstig auf die Performance des Servers auswirken.

Eigene Performancemessungen ergeben hier eindeutig einen Wissensvorteil. Zudem sollte man bedenken, dass größere Datenmengen besser in einer Datenbank aufgehoben sind - und man sollte seine Datenverwaltung nochmals gründlich überschlafen.

29.8. Wie kann ich mir den Inhalt der Sessiondaten anzeigen lassen?

Keywords: Session | Inhalt | anzeigen | Daten

Antwort von Daniel T. Gorski

Ähnlich wie die Ausgabe anderer PHP-Hashes (assoziativer Arrays) lassen sich Sessiondaten mittels einer kleinen Schleife ausgeben. Zuvor muss die Session mit session_start() initialisiert werden. Dies bewirkt, dass der $_SESSION-Hash gefüllt wird.

<?php
    @session_start();
    foreach ($_SESSION as $key =>$value) {
         echo $key." = ".$value."<br>";
    }
?>

29.9. Wie kann ich mir den Inhalt der Cookiedaten anzeigen lassen?

Keywords: Session | Cookie | Daten | anzeigen | Inhalt

Antwort von Daniel T. Gorski

Analog zu obigen Beispiel kann man über $_COOKIE auf den Inhalt eines Cookies zugreifen.

<?php
    @session_start();
    foreach ($_COOKIE as $key => $value) {
       echo $key." = ".$value."<br>";
    }
?>

29.10. Was geschieht im Filesystem des Servers, wenn ich Sessions benutze?

Antwort von Daniel T. Gorski

Damit der Vorgang der Sessionspeicherung ein wenig verdeutlicht wird, untersuchen wir die Veränderungen im Filesystem des Servers. Die folgenden Beispiele laufen auf einem Linux/Unixsystem. Um sie nachvollziehen zu können, muss PHP4 mit safe_mode =Off konfiguriert werden. Zusätzlich wird davon ausgegangen, dass der Client (Browser) Cookies akzeptiert.

Als allererstes braucht man ein Script, welches den Inhalt des im session.save_path festgelegten Directories anzeigen kann:

<?php
    // Datei: list.dir.php
    // Liste das "session.save_path" Directory
    // "safe_mode" muss "Off" sein
    if (strtolower(session_module_name()) == "files")
        {
            echo "<pre>";
            system("ls -l " . session_save_path());
            echo "</pre>";
        }
?>

Dieses Script wird unter dem Namen list.dir.php gespeichert. Hat man z.B. Shell-Zugriff auf den benötigten Teil des Servers, führt natürlich auch ein einfaches ls -l /tmp zu der von list.dir.php produzierten Ausgabe.

Das obige Script wird ausgeführt, um den Ursprungszustand des im session.save_path gespeicherten Verzeichnisses zu betrachten. Zunächst produziert das Script folgendes:

Ausgabe (mit list.dir.php):

    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock

Das Verzeichnis (in diesem Fall /tmp) ist fast leer. Die Datei mysql.sock kann für unseren Zweck vernachlässigt werden.

Als nächstes schreibt man ein Script, welches nach und nach ergänzt oder verändert wird, um die Veränderungen im Filesystem zu beobachten. Der Name diese Scriptes sei session.php. Dieses Script enthält nur die Funktion session_start() . Nach dem Start beobachten wir die Veränderungen:

<?php
    // Datei: session.php
    @session_start();
?>

Ausgabe (mit list.dir.php):

    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www    0 Okt  1 17:58 sess_cd45a3f76f73250..

Wie man sehen kann, hat PHP eine Sessiondatei mit den Zugriffsrechten des Webservers erzeugt, dessen Prozess es ja ist. Diese Datei ist zunächst 0 (Null) Byte groß. Diese Datei wird gleich die Sessiondaten aufnehmen.

Als nächstes wird in dem Script auf die Sessionvariablen zugegriffen, wodurch diese automatisch erstellt werden.

<?php
//  Datei: session.php
    @session_start();

    $_SESSION['userName']        = "dtg";
    $_SESSION['userPermissions'] = "keine :=(";
?>

Ausgabe (mit list.dir.php):

    insgesamt 4
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www   55 Okt  1 17:58 sess_cd45a3f76f73250..

Die Sessiondatei ist nun auf 51 Byte angewachsen. Sie enthält folgenden String:

    userName|s:3:"dtg";userPermissions|s:9:"keine :=(";

also die serialisierten ( serialize() ) Namen und Inhalte der Variablen, die mit session_register() bestimmt wurden. In den Sessiondaten können verschiedene Variablentypen gespeichert werden, also auch Arrays und Objekte.

Wenn man jetzt auf eine Variable in der Sessiondatei verzichten will, kann man mittels unset() PHP davon abhalten, die betreffende Variable zu speichern:

<?php
    // Datei: session.php
    @session_start();
    unset($_SESSION['userPermissions']);

?>

Ausgabe (mit list.dir.php):

    insgesamt 4
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www   21 Okt  1 17:59 sess_cd45a3f76f73250..

Die Variable $_SESSION['userPermissions'] wurde verworfen, die Sessiondatei ist nur noch 19 Byte groß und enthält nur noch die Variable $_SESSION['userName']:

    userName|s:3:"dtg";

Möchte man nun alle Sessiondaten löschen, stellt PHP4 die Funktion session_destroy() zur Verfügung. Vor einem session_destroy() muss aber ein session_start() ausgeführt werden:

<?php
    // Datei: session.php
    @session_start();
    session_destroy();
?>

Ausgabe (mit list.dir.php):

    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock

Die Sessiondatei wurde gelöscht und die Sessionvariablen stehen für weitere Scripte nicht mehr zur Verfügung. Wird nach diesem Zeitpunkt nochmals die Funktion session_start() im gleichen Browserfenster aufgerufen, wird die Sessiondatei erneut erzeugt, und zwar u.U. mit dem gleichen Namen d.h. mit der gleichen Session-ID(!). Der Grund dafür ist, dass der vom Browser gespeicherte Cookie - der die Session-ID enthält - erneut an den Server übermittelt wird.

29.11. Wie benutze ich die Session-Funktionen unter Windows?

Antwort von Sebastian Bergmann

Der Wert von session.save_path in der php.ini ist in seiner Standardeinstellung für den Einsatz der session_*() Funktionen auf Win32 unbrauchbar. Abhilfe schafft hier das Anlegen eines neuen Verzeichnisses, zum Beispiel c:\php4\sessions\, dessen Ort man nun in der php.ini angibt:

session.save_path = c:/php4/sessions

29.12. Was sind Sessions und warum sind sie nützlich?

Antwort von Kristian Köhntopp

Bei einer Session wird jedem Browser, der auf eine Webanwendung zugreift, eine Kennnummer gegeben, mit der man folgende Zugriffe dieses Browsers wiedererkennen kann. Auf dem Webserver werden unter dieser Kennnummer eine Reihe von PHP-Variablen gespeichert, die auf diese Weise von Seite zu Seite weitergereicht werden. Man erzielt damit einen ähnlichen Effekt wie mit <INPUT TYPE="hidden">-Variablen, die von Seite zu Seite weitergereicht werden, vermeidet jedoch eine Reihe von Nachteilen dieser Variablen:

  • Sessionvariablen sind in der Größe nicht durch etwa die Länge der URL oder andere Faktoren beschränkt.

  • Sessionvariablen werden nicht ständig zwischen dem Browser und dem Webserver hin- und hergebounced und sind daher der Manipulation eines Anwenders entzogen, sobald sie erst einmal Bestandteil der Session sind.

  • Sessionvariablen erlauben die Entwicklung weitergehender Nutzanwendungen wie zum Beispiel Benutzeranmeldungen, Warenkörbe oder andere Dienste, die einen Zustand bewahren müssen.

Eine Webanwendung muss man sich aus zwei getrennten Teilen bestehend vorstellen. Der eine Teil, bestehend aus dem Webserver und allen anderen involvierten Rechnern auf dieser Seite der Firewall, ist grundsätzlich vertrauenswürdig. Daten, die von solchen Maschinen kommen, sind Bestandteil des durch den Serveradministrator kontrollierten Bereiches und daher mit großer Wahrscheinlichkeit korrekt und nicht kompromittiert.

Der andere Teil ist alles jenseits der Firewall, einschließlich des Browsers des Benutzers. Daten, die von dort kommen, sind nicht vertrauenswürdig:

Bei konventionellen Anwendungen werden mit jedem GET- oder POST-Request Parameter an den Server gesendet. Es handelt sich um sichtbare Formulardaten oder versteckte Formulardaten aus HIDDEN-Feldern oder statisch an URLs angehängte GET-Parameter sowie um Cookies. Diese Daten muss der Server jedes Mal wenn er sie erhält validieren und gegebenenfalls ablehnen. Das ist sehr, sehr schwierig zu machen und die meisten Anwendungen machen es nicht korrekt. Sie haben daher scheunentorgroße Zugangslöcher, die ein Anwender zum Hijacken der gesamten Webservermaschine benutzen kann. Diese Maschine wird dann zum Einfallstor für das gesamte Subnetz, in dem sich diese Maschine physikalisch befindet.

Bei Anwendungen mit Sessions wird bei jedem Request die Session-ID gesendet und diese hat eine Form, die sie schwer manipulierbar macht. Manipuliert der Benutzer die Session-ID, so erwischt er mit an Sicherheit grenzender Wahrscheinlichkeit eine ungültige Session-ID und startet so eine neue Session. Die Anwendung initialisiert sich dann korrekt und der User startet, als hätte er die Anwendung soeben frisch aufgerufen. Manipulation der Session-ID wird also von der Anwendung erkannt und führt zu einem definierten und unschädlichen Verhalten.

Außerdem erhalten auch Anwendungen mit Sessions ebenfalls Daten aus Formularen. In der Regel sind dies keine HIDDEN-Felder, denn der Zustand wird nun auf dem Server als Teil der Session gehalten. Stattdessen handelt es sich ausschließlich um sichtbare Formulardaten. Diese müssen beim Übergang aus dem Kontrollbereich des Benutzers in die Session natürlich validiert werden. Danach verbleiben sie jedoch in der Session und können als vertrauenswürdig angesehen werden.

Vertrauen bedeutet in diesem Kontext, dass die Daten mindestens die zugesicherten Eigenschaften haben, auf die bei der Validierung getestet worden ist. Die Anwendung darf natürlich nicht darauf vertrauen, dass die Daten Eigenschaften haben, die nicht validiert worden sind. Wenn man bei der Übernahme von Formulardaten in Sessiondaten einen Benutzernamen auf \w{3,8} testet, dann kann man in der Anwendung zwar darauf vertrauen, dass der Benutzername keine bösen Sonderzeichen enthält, nicht leer ist und in die Datenbankfelder passt, aber nicht darauf vertrauen, dass der Benutzername auch die Eigenschaft "eindeutig" und "noch nicht vergeben" erfüllt.

29.13. Wie speichere ich Objekte in Sessions?

Antwort von Clemens Koppensteiner

Man kann Objekte genau wie alle anderen Typen in Sessions speichern indem man sie in den $_SESSION-Array speichert. Zu beachten ist dabei allerdings, dass auf jeder Seite, in der auf die Session zugegriffen wird (in der also session_start() aufgerufen wird), auch die Klassendefinition eingebunden sein muss. Falls sich diese in einer seperaten Datei befindet, muss sie vor session_start() inkludiert werden. Wenn keine Klassendefinition vorhanden ist, stellt PHP die Klassenfunktionen nicht wieder her - die Klasse ist somit meistens nutzlos.

Oft muss man ein Objekt aufräumen, bevor es am Ende des Skriptes gespeichert (serialisiert) wird. Dazu dient dient die "magische" Funktion __sleep() . Diese wird vor dem Serialisieren des Objekts von PHP aufgerufen. Sie muss einen Array mit allen Variablen des Objektes zurückgegben, die gespeichert werden sollen.

Ihr Gegenstück ist die Funktion __wakeup() . Sie wird beim Wiederherstellen des Objektes aufgerufen.

class myClass {

var $id;
var $text;
var $db;

function getText() {
	return $this->text;
}

function setText($text) {
	$this->text = $text;
	// schreibe Text in die Datenbank
	[...]
}

function __sleep() {
	// schließe die Datenbankverbindung
	$this->db->disconnect();
	
	return array('id', 'text');
}

function __wakeup() {
	// stelle die Datenbankverbindung wieder her
	$this->db = new DB([...]);

	// hole den Text aus der Datenbank
	[...]
}
}

29.14. Soll die Session-ID in URL-Parametern oder Cookies gespeichert werden?

Keywords: Cookie | GET | Session | SID | Propagation | Grundlage

Antwort von Kristian Köhntopp

Jede Form von Session-Management basiert auf zwei grundlegenden Dingen:

  • Einer Session-ID, die von Seite zu Seite weitergegeben wird.

  • Einem Datensatz mit Variablen, der auf dem Server in einem dauerhaften Speicher verbleibt und der mit der Session-ID angesprochen wird.

Grundsätzlich gibt es zwei Methoden, die Session-ID von einer Seite zu nächsten weiterzugeben: Entweder die ID wird per Cookie "unsichtbar" als Bestandteil des Requests weitergegeben, oder sie wird auf irgendeine Weise Bestandteil der URL, etwa als GET-Parameter mit einem ? an die URL angehängt, als PATH_INFO an die URL angehängt, als regulärer Pfadbestandteil, der von mod_rewrite herausgepult wird oder als Bestandteil des Hostnamens mit einem Wildcard A-Records im DNS. PHP unterstützt direkt die Weitergabe der Session-ID als Cookie und als GET/POST-Parameter, über mod_rewrite und einige minimale Änderungen ist jedoch auch die Weitergabe als Pfadbestandteil oder Hostname möglich.

Ist die Session-ID in irgendeiner Form Bestandteil der URL, bekommt man das Problem, dass die URL mit der Session-ID gebookmarked wird oder - schlimmer - irgendwo abgedruckt wird. In diesem Fall kann es dazu kommen, dass zwei Benutzerdie nichts miteinander zu tun haben, dieselbe Session verwenden. Dies kann bei Cookies niemals der Fall sein. Daher ist es aus technischer Sicht auf jeden Fall günstiger, Cookies zur Propagation der Session zu verwenden.

Die Presse und schlecht informierte Verbraucherschützer haben Cookies jedoch einen schlechten Ruf eingebracht. Dort wird behauptet, Cookies seien üble Instrumente, um den Kunden zu tracken und sein Verhalten im Web auszuspionieren. Tatsächlich ist es so, dass man wiedererkennbare Benutzer und ihr Verhalten aufzeichnen und auswerten kann - ohne Wiedererkennung sind jedoch auch keine Warenkörbe, personalisierte Websites oder andere individuelle Services möglich.

Andererseits schützt das Ablehnen eines Cookies auch nicht vor dem Tracking und dem folgenden Auswerten des Benutzerverhaltens: Wie oben gezeigt, kann man Session-IDs auch auf andere, schlechtere Weise als durch Cookies weiterverbreiten. Wenn es eine Website also darauf abgesehen hat, einen Benutzer zu tracken, dann hilft das Ablehnen des Cookies exakt gar nichts. Stattdessen ist es notwendig, den Kontakt zu dieser Website vollständig abzubrechen (dies ist insbesondere dann der Fall, wenn man sich nicht von einer Banneragentur wie DoubleClick ausspionieren lassen möchte. Ablehnen des Cookies nutzt auch hier nichts. Stattdessen muss man sich einen Proxy wie etwa JunkBuster installieren und auf diesem allen Datenverkehr in Richtung DoubleClick erden). Anders gesagt: nicht die Cookies sind böse, sondern das, was manche Firmen damit und jeder anderen Form von eindeutiger Identifikation anstellen.

In den Fragen "Wie stelle ich fest, ob der Client die Cookie-Annahme verweigert?" und "Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback?" wird genauer eingegangen, wie man sich flexibel den Einstellungen der Benutzer anpassen kann.

29.15. Warum verwendet PHP nicht die IP-Nummer des Browsers als Schutz gegen eine Übernahme der Session?

Antwort von Kristian Köhntopp

PHP versucht, Sessions Benutzern zuzuordnen. IP-Nummern sind konstruktionsbedingt immer den Netzwerkinterfaces von Rechnern und nicht Benutzern zugeordnet. Das ist eine vollkommen andere Sache und die Auswertung von IP-Nummern würde zu Fehlern im Betrieb führen.

  • Ein Rechner kann mehr als einen Benutzer haben. Jeder Benutzer auf diesem Rechner würde mit derselben IP-Nummer arbeiten. Ein Beispiel ist der Rechner kruuna.helsinki.fi:

    kris@valiant:~ > finger @kruuna.helsinki.fi  | wc -l
        114
    

    Auf diesem Rechner sind zum Messzeitpunkt über 110 Benutzer angemeldet gewesen, die alle über dieselbe IP-Nummer arbeiten.

  • Ein Rechner kann mehr als einen Benutzer repräsentieren. Bei vielen Providern greifen Benutzer über Proxy-Server auf das Netz zu. Nach außen scheinen alle Zugriffe aus dem Netz des Providers von dem Proxy-Server zu kommen. Wenn der Proxy des Providers die Anonymisierungsfunktionen eingeschaltet hat, die z.B. der meistverwendete Proxy, squid2, ab Werk mitbringt oder die Programme wie WebWasher und JunkBuster bieten, dann sind diese Anwender auch durch weitere Header nicht zu unterscheiden.

  • Die sichtbare IP-Nummer eines Benutzers kann während der Session wechseln. Viele Proxy-Server arbeiten in einem Cache-Verbund mit Lastverteilung. Die nach außen sichtbare IP-Nummer eines Anwenders wird je nach Lastsituation im Cache-Verbund diejenige IP-Nummer des am wenigsten ausgelasteten Proxy-Servers sein.

  • Die tatsächliche IP-Nummer eines Benutzers kann während der Session wechseln. Viele Anwender arbeiten mit Timeout bei Inaktivität und dynamisch zugeteilten IP-Nummern. Lässt ein Anwender seinen Browser einige Minuten ungenutzt stehen, wird sich die IP-Verbindung abbauen. Dem Anwender wird beim Neustart der Netzverbindung unter Umständen eine neue, andere IP-Nummer zugeteilt.

Auch eine Auswertung der Headerzeile X-Forwarded-For, die manche Proxies setzen, ist nicht sinnvoll:

  • Diese Headerzeile kann gesetzt sein, muss jedoch nicht vorhanden sein. Squid, Webwasher und Junkbuster entfernen diese Headerzeile, wenn dies gewünscht wird.

  • Die Information in dieser Headerzeile ist weder authentisch noch eindeutig: Die verwendete IP-Nummer kann die IP-Nummer eines RFC-Netzes sein - es gibt also sehr viele Maschinen auf der Welt mit der IP-Nummer 192.168.1.1.

29.16. Wie kann ich Reloads durch den User erkennen und verhindern?

Antwort von Kristian Köhntopp

Gewöhnlich macht man dies, indem man mit Session arbeitet und bei jedem Formular eine eindeutige ID ("Challenge") als Hidden-Variable in das Formular mit aufnimmt, die man sich außerdem in einer lokalen Sessionvariablen auf dem Server merkt.

Wenn das Formular abgesendet wird, vergleicht man die gelieferte Challenge mit der lokal gemerkten Challenge und akzeptiert das Formular nur dann, wenn beide übereinstimmen.

Wenn das Formular verarbeitet wird, löscht man die Challenge in der Sessionvariablen nach Abschluss der Verarbeitung. Wird das Formular ein weiteres Mal versendet, liefert es die alte Challenge aus der Hidden-Variable, deren Gegenstück in der Sessionvariablen aber bereits gelöscht wurde.

Man kann dies sehr schön mit einem generischen Formularvalidator automatisieren, dann hat man gar keine Arbeit mehr damit.

30. PEAR

30.1. Was ist PEAR?

Antwort von Martin Jansen

PEAR (PHP Extension and Application Repository) ist ein zentrales Archiv für Klassen und Bibliotheken in PHP mit einem hohen Wiederverwendungswert. Innerhalb von PEAR existiert darüber hinaus PECL, in dem Erweiterungen für PHP, die in C und C++ geschrieben sind, enthalten sind.

PEAR ist Malin Bakken gewidmet. Die ersten Bestandteile vom PEAR wurden kurz vor ihrer Geburt geschrieben.

Der Zweck von PEAR ist die Verbreitung nützlicher, geprüfter und qualitativ hochwertiger objekt orientierter Skripte (Packages) in PHP und Erweiterungen in C, die von allen PHP-Entwicklern genutzt werden können.

Gleichzeit sollen die Standards, die in PEAR definiert werden, dazu dienen, dass Entwickler Code schreiben können, der portabel, auf vielen anderen Systemen und Konfigurationen einsetzbar und gut lesbar ist.

Wer ein Package zu PEAR beisteuern will, der sollte die Mailingliste pear-dev@lists.php.net abonnieren und sich mit den PEAR Coding Standards auseinandersetzen.

30.2. Wo kann ich PEAR downloaden?

Antwort von Sebastian Bergmann

Der Kern von PEAR ist Bestandteil jeder PHP Distribution seit dem Release von PHP 4.0.0. Zu diesem Kern gehört unter anderem der PEAR Installer, mit dem PEAR Pakete heruntergeladen, installiert und verwaltet werden.

In PHP Distributionen bis einschließlich derer der PHP 4.2 Versionsfamilie ist die mitgelieferte Infrastruktur unbrauchbar und muss über go-pear aktualisiert werden, um den PEAR Installer verwenden zu können.

Auf der Homepage von PEAR, finden sich Informationen zur PEAR Infrastruktur, sowie zu den einzelnen Paketen.

30.3. Wie installiere ich PEAR?

Antwort von Martin Jansen

Unix:

Bei "make install" wird, sofern nicht die --without-pear Direktive bei der Konfiguration angegeben wurde, der Kern des PEAR automatisch nach "/usr/local/lib/php/" kopiert.

Danach muss lediglich die Direktive include_path von

include_path	= ""

nach

include_path	= ".:/usr/local/lib/php/"

geändert werden. Man kann dann durch Aufrufen von pear in einer Shell den PEAR Installer starten.

Windows:

Unter Windows gibt es derzeit keinen automatischen Installationsprozess, daher ist hier etwas Handarbeit gefragt:

  • Download der PHP Distribution oder Download aus dem CVS-Repository

  • Entpacken des Archivs in ein beliebiges Verzeichnis

  • Nun muss die Datei PEAR.php.in im Unterverzeichnis pear in PEAR.php umbenannt werden.

  • In der Datei PEAR.php müssen nun die Pfadangaben in den drei Zeilen

      define('PHP_BINDIR', '@prefix@/bin');
      define('PEAR_INSTALL_DIR', '@PEAR_INSTALLDIR@');
      define('PEAR_EXTENSION_DIR', '@EXTENSION_DIR@');
      

    an die Gegebenheiten der Installation angepasst werden.

  • Zum Abschluss sollte wie bei der Unix-Installation der Include-Path auf den richtigen Wert gesetzt werden.

30.4. Wie nutze ich den PEAR Installer?

Antwort von Sebastian Bergmann

Mit Hilfe des PEAR Installers können PEAR Pakete komfortabel heruntergeladen und verwaltet werden. Damit der PEAR Installer verwendet werden kann, muss PHP mit dem Command Line Interface (CLI) SAPI übersetzt und installiert worden sein. Bis einschließlich den Releases der PHP 4.2 Versionsfamilie ist dies standardmäßig nicht der Fall, so dass PHP vor der Übersetzen mit der --enable-cli Direktive konfiguriert werden muss, damit die Kommandozeilenversion von PHP installiert wird.

Folgender Aufruf lädt und installiert das fiktive PEAR Paket "PHP_FAQ_Beispiel": pear install PHP_FAQ_Beispiel

Wurde bereits eine frühere Version des "PHP_FAQ_Beispiel" Paketes (mit dem PEAR Installer) installiert, so kann ein Upgrade mit pear upgrade PHP_FAQ_Beispiel durchgeführt werden.

pear uninstall PHP_FAQ_Beispiel deinstalliert das Paket wieder.

30.5. Wo finde ich weitere Informationen zu PEAR?

Antwort von Martin Jansen

Die erste Anlaufstelle sollte das PEAR Handbuch sein, dass auf http://pear.php.net/manual/ gelesen werden kann. Es enthält zum einen grundlegende Informationen über den Aufbau des PEAR, über die Möglichkeit und sich aktiv an der Entwicklung zu beteiligen. Des weiteren sind dort viele Packages dokumentiert.

Auf der PEAR Website sind daneben Links zu einer Vielzahl an Tutorials zu PEAR aufgelistet.

31. Open Publication License

31.1. Englische Version

Antwort von Kristian Köhntopp

REQUIREMENTS ON BOTH UNMODIFIED AND MODIFIED VERSIONS

The Open Publication works may be reproduced and distributed in whole or in part, in any medium physical or electronic, provided that the terms of this license are adhered to, and that this license or an incorporation of it by reference (with any options elected by the author(s) and/or publisher) is displayed in the reproduction.

Proper form for an incorporation by reference is as follows:

Copyright (c) 2000-2002 by Kristian Köhntopp, Tobias Ratschiller and others. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/), not using any of the Open Publication License Options from section LICENSE OPTIONS.

Commercial redistribution of Open Publication-licensed material is permitted.

Any publication in standard (paper) book form shall require the citation of the original publisher and author. The publisher and author's names shall appear on all outer surfaces of the book. On all outer surfaces of the book the original publisher's name shall be as large as the title of the work and cited as possessive with respect to the title.

COPYRIGHT

The copyright to each Open Publication is owned by its author(s) or designee.

SCOPE OF LICENSE

The following license terms apply to all Open Publication works, unless otherwise explicitly stated in the document.

Mere aggregation of Open Publication works or a portion of an Open Publication work with other works or programs on the same media shall not cause this license to apply to those other works. The aggregate work shall contain a notice specifying the inclusion of the Open Publication material and appropriate copyright notice.

SEVERABILITY. If any part of this license is found to be unenforceable in any jurisdiction, the remaining portions of the license remain in force.

NO WARRANTY. Open Publication works are licensed and provided "as is" without warranty of any kind, express or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose or a warranty of non-infringement.

REQUIREMENTS ON MODIFIED WORKS

All modified versions of documents covered by this license, including translations, anthologies, compilations and partial documents, must meet the following requirements:

  • The modified version must be labeled as such.

  • The person making the modifications must be identified and the modifications dated.

  • Acknowledgement of the original author and publisher if applicable must be retained according to normal academic citation practices.

  • The location of the original unmodified document must be identified.

  • The original author's (or authors') name(s) may not be used to assert or imply endorsement of the resulting document without the original author's (or authors') permission.

GOOD-PRACTICE RECOMMENDATIONS

In addition to the requirements of this license, it is requested from and strongly recommended of redistributors that:

  • If you are distributing Open Publication works on hardcopy or CD-ROM, you provide email notification to the authors of your intent to redistribute at least thirty days before your manuscript or media freeze, to give the authors time to provide updated documents. This notification should describe modifications, if any, made to the document.

  • All substantive modifications (including deletions) be either clearly marked up in the document or else described in an attachment to the document.

Finally, while it is not mandatory under this license, it is considered good form to offer a free copy of any hardcopy and CD-ROM expression of an Open Publication-licensed work to its author(s).

LICENSE OPTIONS

The author(s) and/or publisher of an Open Publication-licensed document may elect certain options by appending language to the reference to or copy of the license. These options are considered part of the license instance and must be included with the license (or its incorporation by reference) in derived works.

  • To prohibit distribution of substantively modified versions without the explicit permission of the author(s). "Substantive modification" is defined as a change to the semantic content of the document, and excludes mere changes in format or typographical corrections.

    To accomplish this, add the phrase `Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.' to the license reference or copy.

  • To prohibit any publication of this work or derivative works in whole or in part in standard (paper) book form for commercial purposes is prohibited unless prior permission is obtained from the copyright holder.

    To accomplish this, add the phrase 'Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.' to the license reference or copy.

31.2. Deutsche Version

Antwort von Kristian Köhntopp

Inoffizielle deutsche Übersetzung des englischen Originals (von Stefan Meretz).

ERFORDERNISSE FÜR UNMODIFIZIERTE UND MODIFIZIERTE VERSIONEN

Open-Publication-Arbeiten dürfen als Ganzes oder in Teilen reproduziert und verteilt werden, in beliebigen Medien, physisch oder elektronisch, vorausgesetzt, die Bedingungen dieser Lizenz gehören dazu, und diese Lizenz oder ein Verweis auf diese Lizenz (mit jeder Option, die von dem Autor / den Autoren und/oder dem Herausgeber gewählt wurde) wird in der Reproduktion angezeigt.

Eine geeignete Form einer Aufnahme durch Verweis lautet wie folgt:

Copyright (c) 2000-2002 by Kristian Köhntopp, Tobias Ratschiller und weitere Autoren. Dieses Material darf nur gemäß der Regeln und Bedingungen wie sie von der Open Publication Licence, Version v0.4, festgelegt werden, verteilt werden (die letzte Version ist gegenwärtig verfügbar unter http://www.opencontent.org/openpub/). Diese Veröffentlichung macht von keiner der im Abschnitt LIZENZ-OPTIONEN genannten Optionen Gebrauch.

Die kommerzielle Weiterverbreitung von Open Publication lizensiertem Material ist zu den aufgeführten Bedingungen ausdrücklich gestattet.

Jegliche Publikation im Standard- (Papier-) Buch-Format erfordert die Zitierung der Original-Herausgeber und Autoren. Die Namen von Herausgeber und Autor/en sollen auf allen äußeren Deckflächen des Buchs erscheinen. Auf allen äußeren Deckflächen des Buchs soll der Name des Original-Herausgebers genauso groß sein wie der Titel der Arbeit und so einnehmend genannt werden im Hinblick auf den Titel.

COPYRIGHT

Das Copyright jeder Open Publication gehört dem Autor / den Autoren oder Zeichnungsberechtigten.

GÜLTIGKEITSBEREICH DER LIZENZ

Die nachfolgenden Lizenzregeln werden auf alle Open-Publication-Arbeiten angewendet, sofern nicht explizit anders lautend im Dokument erwähnt.

Die bloße Zusammenfassung von Open-Publication-Arbeiten oder eines Teils einer Open-Publication-Arbeit mit anderen Arbeiten oder Programmen auf dem selben Medium bewirkt nicht, dass die Lizenz auch auf diese anderen Arbeiten angewendet wird. Die zusammengefaßte Arbeit soll einen Hinweis enthalten, die die Aufnahme von Open-Publication-Material und eine geeignete Copyright-Notiz angibt.

ABTRENNBARKEIT. Wenn irgendein Teil dieser Lizenz durch irgendeine Rechtsprechung außer Kraft gesetzt werden, bleiben die verbleibenden Teile der Lizenz in Kraft.

KEINE GEWÄHRLEISTUNG. Open-Publication-Arbeiten werden lizensiert und verbreitet "wie sie sind" ohne Gewährleistung jeglicher Art, explizit oder implizit, einschließlich, aber nicht begrenzt auf, der impliziten Gewährleistung des Vertriebs und der Geignetheit für einen besonderen Zweck oder eine Gewähleistung einer non-infringement.

ERFORDERNISSE FÜR MODIFIZIERTE ARBEITEN

Alle modifizierten Versionen, die durch diese Lizenz abgedeckt werden, einschließlich von Übersetzungen, Anthologien, Zusammenstellungen und Teildokumenten, müssen die folgenden Erfordernisse erfüllen:

  • Die modifizierte Version muss als solche gekennzeichnet werden.

  • Die Person, die die Modifikationen vornimmt, muss genannt und die Modifikationen müssen datiert werden.

  • Danksagungen der Original-Autors und -Herausgebers - sofern vorhanden - müssen in Übereinstimmung mit der normalen akademischen Zitierungspraxis erhalten bleiben.

  • Der Ort des originalen unmodifizierten Dokuments muss benannt werden.

  • Die Namen der Original-Autoren dürfen nicht benutzt werden ohne die Erlaubnis des Original-Autors / der Original-Autoren.

EMPFEHLUNGEN EINER GUTEN PRAXIS

In Ergänzung zu den Erfordernissen dieser Lizenz, wird von den Weiterverteilenden erwartet und ihnen stark empfohlen:

  • Wenn Sie Open-Publication-Arbeiten als Hardcopy oder auf CD-ROM verteilen, schicken Sie eine E-Mail-Ankündigung Ihrer Absicht der Weiterverteilung mindestens dreißig Tage bevor Ihr Manuskript oder das Medium endgültig festgelegt ist, um den Autoren Zeit zu geben aktualisierte Dokumente anzubieten. Die Ankündigung sollte die Änderungen beschreiben, die gegebenenfalls am Dokument vorgenommen wurden.

  • Alle substantiellen Modifikationen (einschließlich Löschungen) sind entweder im Dokument klar zu kennzeichnen oder sonst in einem Anhang zu beschreiben.

Schließlich, obwohl nicht erforderlich unter dieser Lizenz, ist es, eine vorgeschlagene gute Form eine kostenlose Kopie jedes Hardcopy- und CD-ROM-Ursprungs einer unter Open Publication lizensierten Arbeit dem Autor / den Autoren anzubieten.

LIZENZ-OPTIONEN

Der/die Autor/en und/oder der Herausgeber eines unter Open Publication lizensierten Dokuments darf bestimmte Optionen durch Anhängen von Regelungen an den Lizenz-Verweis oder die Lizenz-Kopie wählen. Diese Optionen sind empfohlener Teil der Lizenzbestimmungen und müssen in abgeleiteten Arbeiten in die Lizenz eingefügt werden.

  • Verhindern der Verteilung von substantiell modifizierten Versionen ohne explizite Erlaubnis des Autors / der Autoren. "Substantielle Modifizierung" ist definiert als eine Änderung des semantischen Inhalts des Dokuments und schließt bloße Format-Änderungen oder typographische Korrekturen aus.

    Zur Anwendung fügen Sie den Satz 'Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder' (Verbreitung von substantiell modifizierten Versionen dieses Dokuments ist ohne die explizite Erlaubnis des Copyright-Inhabers untersagt) dem Lizenz-Verweis oder der Lizenz-Kopie hinzu.

  • Verhindern jeglicher Veröffentlichung dieser Arbeit oder abgeleiteter Arbeiten im Ganzen oder in Teilen in Standard- (Papier-) Buchform für kommerzielle Zwecke ohne vorherige Erlaubnis durch den Copyright-Inhaber.

    Zur Anwendung fügen Sie den Satz 'Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder' (Verbreitung dieser Arbeit oder abgeleiteter Arbeiten in Teilen in Standard- (Papier-) Buchform für kommerzielle Zwecke ohne vorherige Erlaubnis durch den Copyright-Inhaber ist untersagt) dem Lizenz-Verweis oder der Lizenz-Kopie hinzu.

32. PHP 3

32.1. Wie funktioniert ein Datei-Upload über HTML-Formulare bei PHP 3?

Keywords: Datei | Upload | input | POST | enctype | HTML

Antwort von Kristian Köhntopp

Ein Upload-Formular muss ein Input-Element enthalten, das den Typ file hat. Da Dateien in einem Upload prinzipiell beliebig groß werden können, muss die Übermittlung des Formulares mit der Methode POST erfolgen. Außerdem muss ein bestimmter ENCTYPE für das Formular angegeben werden. Ein solches Formular kann von Netscape Navigator ab Version 3 und von Microsoft Internet Explorer ab Version 4 verarbeitet werden.

<h1>Hallo</h1>
<form action="/submit.php3" method="post"
      enctype="multipart/form-data">
<input type="file" name="probe">
<input type="submit" value="los">
</form>

Das empfangende PHP-Script bekommt das Resultat des Datei-Uploads in einer Reihe von globalen Variablen mit dem Namensprefix $probe übermittelt, weil das Input-Element im Formular diesen Namen hat.

$probe

Diese Variable enthält den Namen der Datei in einem temporären Verzeichnis auf dem Server. Sie kann von dort mit einem copy() -Aufruf abgeholt werden. Das ist auch notwendig, da die Originaldatei am Ende des Scriptes automatisch gelöscht wird.

$probe_name

Diese Variable enthält den Namen der Datei auf dem System des Anwenders. Der genaue Dateiname mit evtl. vorhandenen Laufwerksbuchstaben, Pfadseparatoren und anderen Sonderzeichen ist betriebssystemabhängig und das empfangende Script sollte keine Annahmen hierüber machen.

$probe_size

Diese Variable enthält die Länge der Datei auf dem Server in Bytes.

$probe_type

Diese Variable enthält den MIME-Type der Datei, so wie er dem Server vom Browser übermittelt worden ist.

Der Upload von Dateien wird durch die beiden Konfigurationsparameter upload_tmp_dir und upload_max_filesize in der php3.ini gesteuert. Und in Windows muss man sich in der php3.ini dringend ein gültiges upload_tmp_dir definieren, bevor das gezeigte Beispiel funktionieren kann. Des weiteren ist es unter Umständen notwendig, die Variable $probe mit der PHP-Funktion stripslashes() zu bearbeiten, bevor man auf sie mit copy() oder einem anderen Befehl zugreift. Dies liegt daran, dass Windows Pfadangaben scheinbar quotet, was der Befehl stripslashes() wieder rückgängig macht. Der Pfad zu upload_tmp_dir muss absolut angegeben werden.

PHP legt die temporäre Datei in dem angegebenen Verzeichnis an und löscht sie am Ende des Scriptes wieder. Die Datei darf maximal die angegebene Größe haben. Ein Einstellen der Größenbegrenzung begrenzt jedoch nicht wirklich den Plattenplatz, der auf dem Server von PHP durch Fileupload verbraucht wird: Aus technischen Gründen muss PHP die Datei zunächst empfangen und kann sie erst dann verwerfen, wenn sie zu groß ist. Seit PHP 3.0.10 kann mehr als eine Datei pro Formular hochgeladen werden.

Achtung:

Aufgrund eines Bugs in PHP muss das Script nach dem Upload prüfen, ob sich der/die Name(n) des Datei-Formularfeldes (im folgenden Beispiel probe) in den Hashes $HTTP_GET_VARS, $HTTP_POST_VARS oder $HTTP_COOKIE_VARS befindet. Ist dem so, muss das Script die Weiterverarbeitung (Kopieren) der - angeblich - hochgeladenen Datei(en) verweigern.

Mehr über neue Funktionen zum Upload-Handling der PHP Versionen > 3.0.16 und > 4.0.2 gibt es im Kapitel POST method uploads auf www.php.net.

Vollständiges Beispiel:

<h1>Upload</h1>

<form
  action="<?php print $PHP_SELF ?>"
  method="post"
  enctype="multipart/form-data">
<input type="file" name="probe">
<input type="submit" value="Los!">
</form>
<hr>
<?php
  if (isset($probe)) {

    // Bugfix für: http://www.securityfocus.com/archive/1/80106
    if ( isset($HTTP_COOKIE_VARS["probe"]) ||
         isset($HTTP_POST_VARS  ["probe"]) ||
         isset($HTTP_GET_VARS   ["probe"])
       ) die("Aus Sicherheitsgründen stirbt das Script jetzt.");

    copy($probe, "./newfile.txt");
    printf("Die Datei %s steht jetzt als"
          ." newfile.txt zur Verfügung.<br>\n",
      $probe_name);
    printf("Sie ist %s Bytes groß und vom Typ %s.<br>\n",
      $probe_size, $probe_type);
  }
 ?>

32.2. Ich verwende PHP (Version 3) als Apache-Modul. Wie kann ich dies konfigurieren?

Antwort von Kristian Köhntopp

Immer wenn von einer Konfigurationsanweisung der Form name=wert für die Konfigurationsdatei php3.ini die Rede ist, dann kann für mod_php in Apache stattdessen auch die Apache-Konfigurationsdirektive php3_name wert verwendet werden. Man beachte, dass dem Namen der Anweisung php3_ vorangestellt wird und dass in einer Apache-Konfigurationsdatei keine Gleichheitszeichen verwendet werden dürfen.

Die Konfigurationsdirektiven können in der httpd.conf in einem <directory>-Block oder in einer .htaccess-Datei stehen. Sie gelten für das bezeichnete Verzeichnis und alle seine Unterverzeichnisse. Dies erlaubt es, die PHP-Konfiguration pro Verzeichnis anzupassen.

Mit Hilfe der PHP-Funktion get_cfg_var.php() lassen sich Konfigurationsvariablen aus der php3.ini zur Laufzeit abfragen.

32.3. Wie kann ich Duplikate aus einem Array entfernen?

Keywords: Mengen

Antwort von Johannes Frömter

Da es die Funktion array_unique() erst ab PHP 4.0.1 gibt, muss man sich in PHP wie folgt behelfen:

function array_unique($a) {
    while (list($k, $v) = each($a)) $b[$v] = "";
    while (list($k, $v) = each($b)) $c[] = $k;
    return $c;
}

Valid HTML 4.01! Valid CSS!

alles in einer einzigen Datei
http://www.php-faq.de/faq-single.html
de.comp.lang.php.* FAQ | (c) Copyright 2000-2007 Das dclp-FAQ-Team