Een bestand uploaden met PHP
Sinds jaar en dag biedt PHP je de mogelijkheid om geuploadde bestanden te verwerken. File uploads stellen jouw gebruikers in staat om bijvoorbeeld een user icon of gewoon een foto te uploaden. In dit artikel leer je om te gaan met de file-upload feature zoals deze beschikbaar is sinds PHP 4.2.0; we werken dus met de $_FILES superglobal.
Onder het motto "weg met de oude artikelen" is dit een herschreven versie van een artikel van mij dat ooit op PHPFreakz is geplaatst. Deze herschreven versie heb ik inmiddels ook bij hun aangeboden.
Update: Al bekend met standaard file uploads en benieuwd hoe je een progressbar kunt maken? Lees dan ook onze file upload progress bar met PHP tutorial.
Hoe upload je een bestand?
Allereerst zul je een simpel formulier moeten maken waarin je aangeeft dat je graag wilt dat de gebruiker een bestand selecteert om te uploaden. Dit doe je met het file input element:
Wanneer je nu op verzend klikt zal de browser een POST-request in elkaar draaien (hoe dat er precies uit ziet zie je later). Het bestand komt uiteindelijk bij PHP aan en deze verwerkt de file vervolgens. Om met de file te kunnen werken zet PHP de geuploadde file tijdelijk weg in een directory. Waar dit precies is wordt geregeld met de upload_tmp_dir configuratie optie.
Belangrijk: Op het moment dat jouw script klaar is met uitvoeren wordt dit geuploadde bestand verwijderd van de server. Kopieer je het bestand bijvoorbeeld niet naar een andere directory, dan ben je het dus kwijt.
Het gebruiken van het geuploadde bestand in PHP
Nadat PHP het bestand heeft weggezet in de tijdelijke directory vult PHP de superglobal $_FILES. Dit is een array met een array van informatie voor elk geupload bestand. Oftewel, het bestand uit het formulier hierboven kunnen we benaderen via $_FILES['mijn_bestand']. Zoals gezegd bevat dit element ook weer een array - en wel met de volgende informatie:
- $_FILES['mijn_bestand']['name'] - Dit element bevat de naam van het bestand zoals deze heette bij de gebruiker
- $_FILES['mijn_bestand']['type'] - Dit element bevat het MIME-type van het bestand zoals bepaald door de browser
- $_FILES['mijn_bestand']['size'] - Dit element bevat de grootte van het bestand in bytes
- $_FILES['mijn_bestand']['tmp_name'] - Dit element bevat het complete pad naar het tijdelijke bestand dat door PHP is gemaakt voor deze upload
- $_FILES['mijn_bestand']['error'] - Dit element bevat een eventuele error code (zie onder)
Het element $_FILES['mijn_bestand']['error'] bestaat sinds PHP 4.2.0 en bevat een van de volgende code waarden:
- UPLOAD_ERR_OK (0) - Er zijn geen fouten opgetreden
- UPLOAD_ERR_INI_SIZE (1) - Het bestand is groter dan de ingestelde waarde voor upload_max_file in php.ini
- UPLOAD_ERR_FORM_SIZE (2) - Het bestand is groter dan de in de HTML opgegeven MAX_FILE_SIZE waarde
- UPLOAD_ERR_PARTIAL (3) - Het bestand is niet geheel geupload
- UPLOAD_ERR_NO_FILE (4)- Er is geen bestand geupload
- UPLOAD_ERR_NO_TMP_DIR (6) - Bestaat sinds PHP 4.3.10: PHP kon de directory voor het tijdelijke bestand niet vinden.
- UPLOAD_ERR_CANT_WRITE (7) - Bestaat sinds PHP 5.1.0: PHP kon het tijdelijke bestand niet wegschrijven
Elk van deze codes is ook een PHP constante, dus deze kun je in je scripts gebruiken om te controleren of alles wel goed gegaan is:
Zoals ik al eerder zei zal PHP aan het einde van het script het bestand verwijderen uit de tijdelijke directory. Om te voorkomen dat je het bestand helemaal kwijt bent kun je dit bestand naar een nieuwe locatie kopieëren. Vanwege de nodige valkuilen die vroeger bestonden in PHP maar ook vanwege de Safe-mode feature in PHP zul je gebruik moeten maken van een speciale functie om dit te realiseren: move_uploaded_file. Wanneer safe-mode aanstaat op een server die jij gebruikt en je probeert de gewoon de copy() functie te gebruiken zul je merken dat PHP dit niet zomaar toestaat. De move_uploaded_file() functie is in staat om deze beveiliging te omzeilen maar alleen voor geuploadde bestanden.
De functie move_uploaded_file accepteert 2 argumenten: het absolute pad naar het tijdelijke bestand en het doel bestand. De functie controleert of het eerste argument wel echt een bestand is dat zojuist door PHP is geaccepteerd als upload-file en verplaatst vervolgens het bestand naar de doel locatie. Lukt het niet of is het eerste argument niet een van de geuploadde files dan geeft deze FALSE terug:
-
/* Een voorbeeld dat de geuploadde file kopieert alszijnde een user icon voor de huidige gebruiker */
-
-
$gebruikersNaam = 'tripham';
-
$doelDirectory = '/home/mathieu/public_html/uploads/';
-
$errorMessage = '';
-
$doelFile = $doelDirectory . 'usericon-' . $gebruikersNaam . '.jpg';
-
-
{
-
if ($_FILES['mijn_bestand']['type'] == 'image/jpeg')
-
{
-
-
if (!$result) {
-
$errorMessage = 'Kan bestand niet kopieren';
-
}
-
} else {
-
$errorMessage = 'Bestand is niet van het juiste type';
-
}
-
} else {
-
$errorMessage = 'Uploaden mislukt';
-
}
Belangrijk: Het gebruik van $_FILES['mijn_bestand']['type'] is niet helemaal veilig omdat deze informatie wordt bepaald door de browser. Kortom, het is informatie die van de gebruiker vandaan komt en die informatie moet je nooit blindelings vertrouwen. Beter zou zijn om bijvoorbeeld de functie mime_content_type() of de extensie fileinfo (PECL) te gebruiken hiervoor.
Maximale grootte opgeven
Wanneer je gebruikers bestanden wilt laten uploaden loop je natuurlijk het risico dat iemand misbruik gaat proberen te maken door bijvoorbeeld een paar hele grote bestanden te uploaden. PHP biedt enkele configuratie opties om dit probleem in te perken. Deze opties zijn veelal in php.ini of een .htaccess bestand te wijzigen.
- upload_max_size - Geeft in bytes de maximale grootte van te uploaden bestanden op
- file_uploads - Geeft aan of je uberhaupt file uploads wilt toestaan.
- post_max_size - Geeft aan hoe groot het POST request (incl. een eventuele upload) maximaal mag zijn.
- max_input_time - Geeft aan, in seconden, hoe lang PHP bezig mag zijn met het verwerken van inkomende data
Achter de schermen
Hoewel het uploaden van een bestand niet bepaald lastig is, gebeurd er achter de gordijnen toch wel het een en ander. We zullen eens kijken wat die browser nu precies aan het uitspoken is als je een bestand upload. Normaal gesproken, als je een formulier met bijvoorbeeld een veld 'naam' verstuurd stelt je browser ongeveer de volgende POST request samen:
-
POST /~mathieukooiman/Tests/test.php HTTP/1.1
-
Host: localhost
-
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6
-
Content-Type: application/x-www-form-urlencoded
-
Content-Length: 41
-
-
naam=Scriptorama.nl&verstuur=Submit+Query
Kortom, de nodige HTTP headers en dan een simpele string die lijkt op de query string uit de URL: http://localhost/index.php?naam=Scriptorama.nl&verstuur=Submit+Query. Dat werkt allemaal prachtig, maar zoals je je misschien kunt voorstellen is het wat lastig om op dezelfde manier een bestand aan te geven. In welk formaat zou je dat moeten doen en hoe geef je aan wat de bestandsnaam is?
Om die problemen op te lossen maakt de browser gebruik van een techniek die je wellicht wel eens eerder gezien hebt bij het toevoegen van een attachment in een mailtje dat je vanuit PHP probeert te verzenden: secties welke allemaal een eigen setje headers hebben en uit elkaar gehouden worden door een zogenaamde boundary. Bij het uploaden van een bestand 'test.txt' verstuurt je browser zo ongeveer het volgende:
-
POST /~mathieukooiman/Tests/ HTTP/1.1
-
Host: localhost
-
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6
-
Content-Type: multipart/form-data; boundary=---------------------------1019292671580723810704877633
-
Content-Length: 372
-
-
-----------------------------1019292671580723810704877633
-
Content-Disposition: form-data; name="test"; filename="test.txt"
-
Content-Type: text/plain
-
-
Bezoek http://www.scriptorama.nl !
-
-----------------------------1019292671580723810704877633
-
Content-Disposition: form-data; name="hatzee"
-
-
Submit Query
-
-----------------------------1019292671580723810704877633--
Zoals je ziet wordt er nu voor elk veld (zelfs de submit knop) een nieuwe sectie aangemaakt. De naam van het veld en de geselecteerde bestandsnaam worden in de Content-Disposition header gedefinieerd.
Conclusie
In dit artikeltje heb je gezien hoe je file-uploads kunt implementeren. Het verwerken van file uploads met PHP is niet bijzonder lastig maar er kunnen nog wat haken en ogen aanzitten.
Volg Scriptorama via RSS!
Reageer ook!
Wil je het OO aanpakken dan heb ik een eenvoudige class die prima werkt. Werken met een OO API kan je code in dit geval behoorlijk leesbaarder maken. Roep maar als je code wilt zien.
Door Michel
op 08.25.06 @ 12:51 pm | Permalink
Misschien ook leuk om te zeggen dat in php5.2 een onderstuining komt om te kijken hoeveel procent er is upgeload ;)
Dit is iets waar we al heel lang op zitten te wachten.
Door Sebastiaan Stok
op 08.25.06 @ 5:18 pm | Permalink
Wat is je bron Sebastiaan? Volgens http://oss.backendmedia.com/PhP52 zie ik daar niks van terug. Daarnaast kan dat helemaal niet met PHP?
Door Tri Pham
op 08.25.06 @ 7:42 pm | Permalink
Ik vraag me ook af hoe je zoiets wil bewerkstelligen. Tijdens de transactie van het bestand neem ik aan dat je niet zomaar even de status kan pollen.
Door Gerard Klomp
op 08.26.06 @ 2:56 am | Permalink
Laat maar komen Michel ;) Ik ben wel eens benieuwd hoe anderen dat aanpakken.
Door brossiekoppie
op 08.26.06 @ 9:21 am | Permalink
Er zijn wel verschillende patches beschikbaar die dit bieden en dit werkt wel degelijk via polling.
Hier is er bijvoorbeeld 1 die alleen een extensie nodig heeft:
http://pdoru.from.ro/
Door Mathieu Kooiman
op 08.26.06 @ 9:37 am | Permalink
Staat in de changelog van PHP5.2, ik zal hem even op zoeken.
Door Sebastiaan Stok
op 08.26.06 @ 1:58 pm | Permalink
http://cvs.php.net/viewcvs.cgi/php-src/NEWS?view=markup&pathrev=PHP_5_2
- Added RFC1867 fileupload processing hook. (Stefan E.)
Door Sebastiaan Stok
op 08.26.06 @ 2:24 pm | Permalink
Sebastiaan, dat lijkt mij iets anders. Een 'hook' heeft meestal te maken met event handling. Ik denk eerder dat je bij een file upload een reeks van instructies aanroept (per definitie). Vanuit de pagina is niet duidelijk of het userland is of vanuit de Zend engine, maar op deze link is te lezen dat het internal is:
http://marc.theaimsgroup.com/?l=php-dev&m=114994413816284&w=2
Dus voorlopig geen progressbar features. Dit lijkt mij ook een 'onnodige' feature voor PHP. Daarnaast moet je imho dit ook niet implementeren in PHP.
Door Tri Pham
op 08.26.06 @ 2:56 pm | Permalink
Grr...
Hè wat jammer nou :(
Nouw ja, die andere nieuwe Exts bij php5.2 zijn ook wel gaaf dus blijft het intresand.
Door Sebastiaan Stok
op 08.27.06 @ 3:19 pm | Permalink
Hoi,
In bovenstaand voorbeeld staat in het PHP voorbeeld:
$result = move_uploaded_file ($FILE['mijn_bestand']['tmp_name'], $doelFile);
moet dit niet zijn:
$result = move_uploaded_file ($_FILES['mijn_bestand']['tmp_name'], $doelFile);
Dackt ik?
Richard
Door Richard
op 01.07.08 @ 2:17 pm | Permalink
Inderdaad, ik heb het gewijzigd. Bedankt voor je melding! :)
Door Mathieu Kooiman
op 01.07.08 @ 2:40 pm | Permalink
Hoi,
Ik heb heel de procedure doorlopen, maar blijkbaar wil het dus niet werken. Ik gebruik WAMP server.
Mijn bestand heb ik upload.php genoemd en de $doelDirectory is relatief. Dus in de map waar upload.php zit, zit er ook een map: bestanden.
Mijn code:
Untitled Document
Door Mathias
op 03.18.09 @ 2:36 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>