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(...))undexplode/endliegen 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
extensionganz. 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, ...).