Fil upload - nemt og effektivt?

Det kan ofte være rart med en løsning hvor du kan tilbyde dine brugere at uploade et billede - enten som profilbillede eller til albums - eller andre typer dokumenter; PDF'er, word dokumenter og så videre.

Men der er nogle gode måder og nogle dårlige måder at gøre det på. Artiklen her vil forsøge at uddybe nogle af de effektive og de mindre effektive løsningsmodeller.
Forudsætningen for at få noget ud af artiklen er basal kendskab til HTML og PHP.

Artiklen er opdelt således:
  1. Det basale om upload
    1. Hvorfor er det vigtigt?
    2. Afsend format med ENCTYPE
  2. Modtagelse af filen
    1. Hvad gør jeg med de data?
    2. Gem de midlertidige data til en fil
  3. Hvad så nu?
  4. Hvordan kommer jeg så videre?
    1. Struktur og overblik
  5. Men der er noget der ikke virker!
    1. Skriverettigheder
    2. Tilladt størrelse på upload

Det basale om upload

Det et uploade en fil skiller sig lidt ud fra den sædvanlige måde at sende POST data på. Årsagen er simpel: der findes nogle retningslinier der foreskriver hvordan det skal gøres - de retningslinier er med til at sikre at udviklerne af de forskellige browsere sender data afsted, til os som modtagere, på samme måde.
Med andre ord, så er det bare sådan det er.

Hvorfor er det vigtigt?

Det er en slags 'nice to know' information at have; som udgangspunkt sendes alle POST data fra klienten til serveren i et format kendt som application/x-www-form-urlencoded. Formatet afgør så, ifølge de føromtalte retningslinier, om du kan sende en fil til serveren eller ej. For at kunne sende en fil, skal du fortælle klienten hvilket format den skal sende din POST data i. Formatet hedder multipart/form-data og skal angives som din forms ENCTYPE.

Afsend format med ENCTYPE

ENCTYPE er en attribut til FORM tagget.
Det nemmeste er lige at vise dig den simple form, som indeholder hvad der skal bruges for at du kan sende filer til serveren.

 <form enctype="multipart/form-data" action="modtagfil.php" method="post">
        Fil: <input type="file" name="minfil" />
        <input type="submit" name="send" value="Send">
    </form>

Ovenstående behøver vist ikke videre uddybelse andet end du nu burde kunne se at ENCTYPE er angivet som en attribut i FORM tagget.
Ydermere bør du også se at der er et INPUT tag af typen 'file'.

Det var lidt basal viden om det at sende en fil - det er jo altid rart at kende lidt til baggrunden for det man gør.

Modtagelse af filen

Vi tager udgangspunkt i HTML eksemplet fra før; en simpel HTML formular med mulighed for at afsende en fil.
Men vi tager lige et par mere grundlæggende skridt, før vi rent faktisk begynder at gøre noget ved filen - for hvad sker der med de data som klienten sender til dig?

Når klienten sender filen mod dit script, vil PHP lægge de midlertidige data tilgængelige et sted (defineret igennem konfiguration) som du kan hente dem fra - dog er de kun eksisterende i den session der er påbegyndt; det betyder - kort sagt - at du bliver nød til at gøre noget for rent faktisk kunne anvende filen, for lige så snart siden er færdig med sin indlæsning er de midlertidige data væk.

Hvad gør jeg med de data?

PHP har en superglobal variabel navngivet $_FILES som indeholder et multidimensionelt array med dine fildata.
Den er faktisk ret nem at anvende og forstå; udfra vores formular fra før, kan jeg se at mit input-felt til filen var navngivet 'minfil'. Det vil også være det array-indeks som jeg skal bruge når jeg vil have filens data: $_FILES["minfil"]

$_FILES["minfil"] indeholder et array med følgende data:

  • [name] => billede.jpg (navnet på filen som kommer fra klienten)
  • [type] => image/jpeg (skulle angive typen på filen, men da det kommer fra klienten skal du altid tænke på din egen sikkerhed)
  • [tmp_name] => /tmp/php/php1s5s88 (stien til filen som indeholder de midlertidige data)
  • [error] => UPLOAD_ERR_OK (det typiske indhold, så den vil vi ikke dække)
  • [size] => 61440 (størrelsen af filen i bytes)

Nu har vi faktisk alt hvad vi skal bruge for at kunne flytte de midlertidige data ned i en fil vi kan anvende senere.

Gem de midlertidige data til en fil

Nu ved vi alt hvad vi skal vide for at kunne gemme filen - så nu tager vi den mest simple og effektive måde at gøre det på og derefter kigger vi lidt på de fejl der kan/vil opstå.

Vi sender - ifølge vores oprindelige HTML formular - filen til modtagfil.php, så vi opretter en fil med det navn og følgende indhold:

<?php

$uploaddir 
"/var/www/uploads/"// Angiv stien hvortil filen skal gemmes - her skal du naturligvis have skriverettigheder til
$uploadfile $uploaddir $_FILES["minfil"]["name"]; // Sammensæt stien samme med filnavnet fra klienten

// Flyt filen fra den midlertidige plads til den plads vi har angivet ovenfor
move_uploaded_file($_FILES["minfil"]["tmp_name"], $uploadfile); 

?>

Ovenstående er et skole-eksempel på hvilke trin der skal til for få en fil lagt ned, hvor du vil have den - såfremt der ikke skulle være noget som kunne fejle under processen...

Problemet er bare at det sjældent er tilfældet - både driftsmæssigt og sikkerhedsmæssigt er ovenstående uforsvarligt. Med ovenstående må dine brugere uploade lige hvad der passer dem - fra et uskyldigt billede til et PHP script der kan nedlægge dit website eller blotte vitale informationer om dit site.

Hvad så nu?

Det er utroligt vigtigt at du forstår at dit udgangspunkt må være at du ikke kan stole på dine brugere - at alle dine potentielle brugere faktisk kun vil skade dig; også selvom det er en lille lukket kreds, for det er absolut ikke sikkert at dine brugere ville gøre det med fuldt overlæg.

Lad os arbejde ud fra at det er et billede du gerne vil lade dine brugere uploade. Hertil burde vi sætte nogle forudsætninger for hvad der er tilladt for dine brugere at uploade. Men vi bliver nød til at lave en logisk opdeling - for et er hvad vi fortæller brugeren denne må, noget andet er hvad vi gør rent praktisk.
Forestil dig at vi stiller disse krav til dine brugere: Billedet må maks være 600*400px, det må maks fylde 1MB og billedet skal være enten jpg eller png.
Det er tre relativt simple krav, men vi bliver nød til at tage det trinvist for at kunne sikre os hvad vi finder mest vigtigt - at billedet ikke fylder mere end 1MB og at billedet enten er i jpg format eller png format. Størrelsen vil jeg - af praktiske årsager - først tage stilling til bagefter.

Men lad os tage det vigtige først.
Umiddelbart virker det jo som en fantastisk idé at vi bruger $_FILES["minfil"]["type"] til at kontrollere hvilken type billede det er - vi ved jo at jpg'er typisk har typeangivelsen image/jpg og png'er har typeangivelsen image/png. Men det er desværre data som klienten sender med og vi kan derfor ikke stole på det. Det ville jo være træls at have taget imod et skadeligt PHP script, blot fordi klienten har fortalt os det er et billede, ikke?

Der er mere sikkerhed i at kontrollere filnavnet - altså, om det er den rigtige endelse på filnavnet. Så det bliver det vi gør.
Samtidig skal vi også implementere en kontrol der gør at billedet ikke fylder for meget på serveren - altså, mere end den ene MB.

<?php

    $max_size 
1048576// 1MB
    
$allowed_files = array("jpg""png"); // Filtyper vi tillader

    
    // Vi kontrollerer lige at der overhovedet er sendt noget data
    
if( isset($_FILES["minfil"]) && $_FILES["minfil"]["size"] > )
    {
        
        
//Kontrol af størrelsen
        
if( $_FILES["minfil"]["size"] > $max_size )
        {
            echo 
"Filen er for stor";
        }
        else
        {
            
//Kontrol af filnavn
            
$file_ext strtolowerendexplode("."$_FILES["minfil"]["name"]) ) );
            
            if( !
in_array($file_ext$allowed_files) )
            {
                echo 
"Filen var ikke af den tilladte type";
            }
            else
            {
                
// Alt ok, så vi gemmer filen
                
$uploaddir "/var/www/uploads/";
                
$uploadfile $uploaddir $_FILES["minfil"]["name"];
                
                if( 
move_uploaded_file($_FILES["minfil"]["tmp_name"], $uploadfile) )
                {
                    echo 
"Filen blev gemt";
                }
                else
                {
                    echo 
"Filen kunne ikke gemmes - måske et problem med skriverettigheder";
                }
            }
        }                
    }
        
?>

Nu har du faktisk gemt en fil, ud fra de tekniske kriterier som var muligt gennem $_FILES arrayet.

Hvordan kommer jeg videre?

Som du nok kan se, er det en relativt simpel ting at gemme en fil. Men vi skal jo også bruge filen - blandt andet satte vi et kritiere tidligere der hed sig at billedet ikke måtte være større end 600*400px, så det synes jeg da lige vi skal gøre.

Struktur og overblik

Et generelt problem for mange nye udviklere er at de glemmer at danne sig en meningsfyldt struktur - jeg kan give talrige eksempler på koder som skal uploade et billede, lave kontroller af størrelse og type, skalere og efterbehandle i en lang pærevælling af rodede if-sætninger; så en bøn til dig er du skal dele dine ønsker logisk op - gem billede, behandl billede, brug billede.

Vi har allerede gemt billedet, så nu må vi behandle det, så det passer til vores behov.
For at holde det simpelt laver vi en simpel opdeling i den samme fil, så der stadig er en struktur og mulighed for at danne sig et overblik.
Efter vi har gemt billedet, så kontroller vi størrelsen: <?php

    $max_size 
1048576;
    
$allowed_files = array("jpg""png");

    
// Gem billede

    
if( isset($_FILES["minfil"]) && $_FILES["minfil"]["size"] > )
    {
        
        if( 
$_FILES["minfil"]["size"] > $max_size )
        {
            echo 
"Filen er for stor";
        }
        else
        {
            
$file_ext strtolowerendexplode("."$_FILES["minfil"]["name"]) ) );
            
            if( !
in_array($file_ext$allowed_files) )
            {
                echo 
"Filen var ikke af den tilladte type";
            }
            else
            {
                
$uploaddir "/var/www/uploads/";
                
$uploadfile $uploaddir $_FILES["minfil"]["name"];
                
                if( 
move_uploaded_file($_FILES["minfil"]["tmp_name"], $uploadfile) )
                {                    
                    
// Vi sætter denne for at indikere at vi senere må behandle billedet.
                    
$upload_success true
                }
                else
                {
                    echo 
"Filen kunne ikke gemmes - måske et problem med skriverettigheder";
                }
            }
        }                
    }
    
    
//Behandl billedet
    
    
if( isset($upload_success) )
    {
        
// Vi skaffer de nødvendige oplysninger fra billedet
        
$img_info getimagesize($uploadfile);
        
        
// Endnu en kontrol der kan fortælle om det rent faktisk er et billede
        
if( $img_info === false )
        {
            echo 
"Billedet virker desværre ikke";
        }
        else
        {
            
// Så kontrollerer vi størrelsen
            
if( $img_info[0] > 600 || $img_info[1] > 400 )
            {
                echo 
"Billedet er desværre for stort";
            }
            else
            {
                echo 
"Billedet passer fint i størrelsen";
            }
        }
    }
    
    
/*
        Herefter kan du bruge billedet - at bruge billedet kan være noget så simpelt
        som at tilknytte billedenavnet til et galleri gennem databasen, eller tilknytte
        billedet til en specifik bruger som et probilbillede eller lave thumbnails,
        skalering, dubletter, watermarks - alt hvad din fantasi nu kan begære; for 
        billedet har opfyldt dine krav.
    */
        
?>

Vi har nu en struktureret opdeling af det at vi gemmer billedet, for derefter at lave andre kontroller på dem.
Det er nu nemt at pille en enkelt del ud eller tilpasse den til nye behov, uden at skulle gå alt for mange if-sætninger igennem.
Det er også effektivt fordi der er et struktureret overblik over hvad der skal ske gennem dine tre processer.

Men der er noget der ikke virker!

Der er andre faktorer der spiller ind; skriverettigheder, hukommelse, tilladt størrelse på upload og andre små finurligheder.

Skriverettigheder

Den fejl man hyppigst støder på, er problemer med skriverettigheder - du kan ikke flytte en fil til et bibliotek som du ikke må skrive til. Det giver sig selv.
Den nemmeste måde at løse det på, er at tilgå serveren med FTP og chmod'e mappen til 777.

Tilladt størrelse på upload

Også en fejl som du vil støde på mange gange; du vælger en fil, trykker send og så sker der ikke rigtig mere.
Årsagen er at PHP som standard har en tilladt størrelse på 2MB til at modtage POST data.
Normalt plejer jeg at sætte den op til 20MB (du kan sætte den højere op eller længere ned, hvis du mener tallet er helt hen i hampen) gennem .htaccess:

php_value post_max_size 20M
php_value upload_max_filesize 20M

Hvis du ikke ved hvad .htaccess kan bruges til, vil jeg anbefale dig at læse denne side - den er god og pædagogisk.

Jeg håber det gav anledning til en simpel forståelse for mulighederne med fil uploads samt et overblik over nogle anvendelige teknikker.
Har du spørgsmål, kommentarer, rettelser eller gode idéer til artiklen, som kan forbedre indholdet er du velkommen til at kontakte mig.