PHP: Wie ermittelt man die Dateiendung?

Für die Dateiendung gibt es eine eingebaute Funktion, und die sollte man auch nehmen. Die kurzen Selbstbau-Varianten, die man daneben findet, liefern in zwei sehr alltäglichen Fällen etwas Falsches.

Die Antwort

<?php
  $endung = pathinfo($dateiname, PATHINFO_EXTENSION);
?>

Das ist der Weg. Was dabei herauskommt:

bild.jpg                          -> 'jpg'
archiv.tar.gz                     -> 'gz'
LIESMICH.TXT                      -> 'TXT'
ohne_endung                       -> '' (leer)
.htaccess                         -> 'htaccess'
punkt.am.ende.                    -> '' (leer)
/pfad/zu/datei.php                -> 'php'
ordner.mit.punkt/datei            -> '' (leer)

Zwei Zeilen sind erklärungsbedürftig. Bei archiv.tar.gz bekommst du gz und nicht tar.gz — es gibt in PHP nichts, was mehrteilige Endungen kennt, weil das Dateisystem sie auch nicht kennt. Und bei .htaccess gilt der ganze Name als Endung, weil davor nichts steht.

Warum nicht selbst basteln

Die verbreiteten Alternativen im direkten Vergleich:

                        substr(strrchr)  explode/end      pathinfo
bild.jpg                'jpg'            'jpg'            'jpg'
ohne_endung             ''               'ohne_endung'    ''
.htaccess               'htaccess'       'htaccess'       'htaccess'
archiv.tar.gz           'gz'             'gz'             'gz'
ordner.mit.punkt/datei  'punkt/datei'    'punkt/datei'    ''

Zwei Zeilen weichen ab.

Bei ohne_endung liefert explode/end den gesamten Dateinamen zurück. Das ist der unangenehmere Fehler von beiden, weil ein nicht-leerer String zurückkommt — dein Code merkt also nicht, dass etwas schiefging, und du legst eine Datei mit der Endung ohne_endung an.

Bei ordner.mit.punkt/datei finden strrchr() und explode() einen Punkt, der zum Verzeichnis gehört. pathinfo() weiß, dass es einen Pfad vor sich hat, und trennt richtig. Versionierte Verzeichnisse wie v1.2/ oder Domains als Ordnernamen sind nicht selten.

Der Fall, den man beim Verarbeiten von URLs sofort trifft

Wenn du die Endung aus einer URL ziehst, passiert das hier:

pathinfo('https://example.com/bild.jpg?v=2&x=1', PATHINFO_EXTENSION)
-> 'jpg?v=2&x=1'

Der Query-String hängt mit dran. pathinfo() kann davon nichts wissen, es sieht nur einen String mit einem Punkt darin. Der Umweg:

<?php
  $endung = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
?>
ueber parse_url : 'jpg'

Ein Fragment (#irgendwas) würde übrigens genauso mitgenommen. parse_url() räumt beides weg.

pathinfo() ohne Konstante

Ohne den zweiten Parameter bekommst du ein Array. Und dort fehlt der Schlüssel extension ganz, wenn es keine Endung gibt — er ist nicht etwa leer:

pathinfo('ohne_endung')  keys : ["dirname","basename","filename"]
pathinfo('bild.jpg')     keys : ["dirname","basename","extension","filename"]
isset($p['extension'])        : false

Ein direktes $p['extension'] löst also eine Warning: Undefined array key aus. Entweder mit ?? absichern oder gleich die Konstante verwenden — die liefert in dem Fall einen leeren String.

Groß- und Kleinschreibung

pathinfo() gibt die Endung so zurück, wie sie dasteht:

pathinfo('LIESMICH.TXT') : 'TXT'
strtolower davon         : 'txt'

Wenn du gegen eine Liste erlaubter Endungen prüfst, gehört ein strtolower() dazwischen. Sonst kommt jemand mit .JPG durch deine Prüfung — oder eben nicht durch, obwohl er sollte.

Und jetzt der wichtige Teil: Die Endung sagt nichts über den Inhalt

Das ist der Grund, aus dem ich diesen Artikel überhaupt schreibe. Ein Dateiname ist eine Behauptung, kein Nachweis. Ich habe eine Datei mit PHP-Code darin harmlos.jpg genannt:

Dateiname            : harmlos.jpg
Endung laut pathinfo : jpg
finfo sagt           : text/x-php
getimagesize()       : false

pathinfo() sagt brav jpg. finfo schaut in die Datei hinein und sagt text/x-php. Wenn deine Upload-Prüfung nur die Endung ansieht, liegt jetzt ausführbarer PHP-Code in deinem Uploadverzeichnis.

Der umgekehrte Fall kommt auch vor — ein echtes PNG, das jemand .txt genannt hat:

Endung laut pathinfo : txt
finfo sagt           : image/png
getimagesize()       : 2 x 2, mime image/png

Für Uploads heißt das:

<?php
  $fi  = new finfo(FILEINFO_MIME_TYPE);
  $typ = $fi->file($_FILES['datei']['tmp_name']);

  $erlaubt = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
  ];

  if (!isset($erlaubt[$typ])) {
    throw new RuntimeException('Dateityp nicht erlaubt: ' . $typ);
  }

  // Die Endung selbst vergeben, nicht vom Nutzer uebernehmen.
  $ziel = $verzeichnis . '/' . bin2hex(random_bytes(16))
          . '.' . $erlaubt[$typ];
?>

Drei Dinge daran sind wichtig. Erstens wird der Typ aus dem Inhalt ermittelt. Zweitens ist die Liste eine Positivliste — was nicht drinsteht, ist verboten. Und drittens vergibst du den Dateinamen selbst, statt den vom Nutzer zu übernehmen.

Was du nicht nehmen solltest, ist $_FILES['datei']['type']. Dieser Wert kommt aus dem Upload-Request, also vom Client, und lässt sich genauso frei setzen wie jeder andere Header.

Zusätzlich gehört das Uploadverzeichnis so konfiguriert, dass dort nichts ausgeführt wird — außerhalb des Document Root, oder mit einer entsprechenden Webserver-Regel. Verlass dich nicht darauf, dass die Prüfung allein reicht.

Multibyte-Dateinamen

Kurz, weil die Frage regelmäßig aufkommt:

Übung.pdf  -> pdf
файл.txt   -> txt
日本語.md   -> md

Funktioniert. Der Punkt ist in UTF-8 eindeutig, und die Endung selbst ist in aller Regel ASCII.

Zusammenfassung

  • pathinfo($name, PATHINFO_EXTENSION) und sonst nichts.
  • substr(strrchr(...)) und explode/end liegen bei Dateien ohne Endung und bei Punkten im Verzeichnisnamen falsch.
  • Bei URLs erst parse_url(..., PHP_URL_PATH), sonst hängt der Query-String mit dran.
  • Ohne Konstante fehlt der Schlüssel extension ganz.
  • strtolower() nicht vergessen, wenn du gegen eine Liste prüfst.
  • Beim Upload den Inhalt prüfen (finfo), nicht den Namen — und nicht $_FILES[...]['type'], der kommt vom Client.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).