Home / Indice sezione
 www.icosaedro.it 

 PHP File Upload

Ultimo aggiornamento: 2009-01-04

Primo articolo di una serie di due che esaminano il trasferimento dei file via protocollo HTTP. Qui consideriamo il processo di upload, cioè il trasferimento dei file dal browser al server, con particolare riguardo agli aspetti di sicurezza, alla gestione dei tipi dei file e vedremo anche alcune particolarità note e meno note a livello dei browser, del protocollo e dello sviluppo lato server. Descriveremo gli esempi usando il popolare linguaggio PHP, ma gli argomenti trattati sono più generali e interessano anche gli altri sistemi di sviluppo. Il successivo articolo PHP File Download (www.icosaedro.it/articoli/php-file-download.html) tratta il download dei file via protocollo HTTP.

Indice

Configurare il server
Costruire il FORM
Tipi MIME ed estensioni dei nomi dei file
Come il programma PHP riceve il file
Particolarità sul nome originale del file
Limitare la dimensione dei file in upload
Acquisizione e validazione del file ricevuto
Dove mettere i file caricati?
La barra di avanzamento
Programma di test dell'upload
Come continuare
Bibliografia
Argomenti correlati

Configurare il server

Basilarmente, il file caricato diventa visibile al codice PHP attraverso l'array superglobale $_FILE[]. I dati del file vengono messi qui dopo avere viaggiato dal browser al server WEB, e dal server WEB al PHP. Lato server ci sono parecchi parametri di configurazione rilevanti che riguardano il WEB server e PHP. Cominciamo dal WEB server. Qui considero Apache:

Veniamo ora al PHP vero e proprio. Il suo file di configurazione php.ini contiene molti parametri interessanti:

Costruire il FORM

Per l'upload dei file le specifiche HTML prescrivono di usare il metodo POST del protocollo HTTP con codifica multipart/form-data dei dati. Il control HTML che permette l'upload dei file è "<input type=file". Ecco uno spezzone di codice HTML che permette l'upload di un file:

<FORM method=post
    enctype="multipart/form-data"
    accept="image/gif,image/png,image/jpeg,image/pjpeg"
    action="PROGRAMMA_CHE_RICEVE_IL_POST.php">
Fotografia (formati GIF, PNG o JPEG, max 500 KB):
<INPUT type=hidden name=MAX_FILE_SIZE value=512000>
<INPUT type=file name=FOTO size=50 maxlength=512000><p>
Descrizione:
<INPUT type=text name=DESCR size=50 maxlength=100><p>
<INPUT type=submit name=b value=Salva>
</FORM>

Notiamo in particolare l'attributo enctype="multipart/form-data", indispensabile per l'upload dei file. Se omesso, l'array superglobale $_FILES[] non viene neppure creato.

L'attributo accept=... è opzionale e permette di specificare l'elenco dei tipi MIME per i file che il nostro programma è disposto ad accettare. Il browser può usare questa informazione per filtrare i file nella finestra di dialogo per la selezione del file da inviare. Si tratta quindi di un suggerimento, perciò dal punto di vista della sicurezza non è detto che il tipo MIME del file ricevuto sarà effettivamente tra quelli che compaiono in questo elenco. Purtroppo questa funzionalità non è implementata da tutti i browser:

Il campo hidden MAX_FILE_SIZE può servire per gestire i file troppo lunghi lato PHP. Ne parleremo più avanti in dettaglio.

Passiamo al control per l'upload del file. Il browser mostra il control di tipo type=file tipicamente come un campo di input a linea singola (dove l'utente può battere il nome del file) e un bottone "Sfoglia..." oppure "Browse..." per selezionare da una dialog box il file da inviare. Il nome name=FOTO identifica il file tra i vari dati inviati con il FORM, esattamente come per gli altri control. Un FORM può anche contenere due o più control di invio file: in questo caso i nomi dei control devono essere distinti, oppure si può usare il nome "FOTO[]" con il quale il PHP costruisce automaticamente un array di file inviati. Per l'invio di file multipli occorre comunque predisporre altrettanti control. In verità le specifiche HTML affermano che un singolo control type=file deve consentire l'upload di due o più file, ma purtroppo attualmente nessun browser supporta questa funzionalità, e neppure il PHP la prevede.

L'articolo FORM dell'HTML (www.icosaedro.it/articoli/form.html) spiega in maggior dettaglio come si usano i FORM e come si può realizzare una migliore interfaccia per l'upload dei file.

Tipi MIME ed estensioni dei nomi dei file

Prima di continuare la nostra discussione, premettiamo alcune questioni importanti relative ai tipi MIME e al meccanismo delle estensioni dei file.

Da sempre gli utenti dei PC usano dei sistemi per identificare il tipo di un file. Questa classificazione è utile a molti fini pratici, ma lo è diventata ancora di più con l'introduzione delle interfacce iconiche: se l'utente comanda l'apertura di un file, come dobbiamo intepretare il contenuto di questo file? e con quale programma lo apriamo?

La prima e più semplice delle soluzioni consiste nell'usare il nome stesso del file per contenere queste informazioni. La convenzione comune diventò quella di aggiungere al nome del file un punto, a sua volta seguito da uno o più caratteri alfanumerici che codificano il tipo del file. Questa convenzione, per funzionare bene, richiede la collaborazione delle applicazioni e la disciplina degli utenti. Ad esempio .txt .jpeg .doc .mp3 sono tutte estensioni che oggi sappiamo riconoscere immediatamente.

In alcuni sistemi operativi il concetto di estensione divenne addirittura parte integrante del file system: i sistemi operativi CP/M e DOS riservavano per i nomi dei file un campo a 8 byte e un campo da 3 byte per l'estensione. A livello della interfaccia le due informazioni venivano presentate sempre insieme, con il nome e l'estensione separate da un punto. Era comunque sempre richiesta una certa collaborazione tra le applicazioni e gli utenti per assicurare che queste convenzioni fossero rispettate.



Icone di applicazioni e relativi documenti sul Macintosh System 7. Notare che il nome del file è un testo in prosa scelto liberamente dall'utente. L'informazione su tipo e applicazione creatrice viene invece sintetizzato nell'aspetto della icona: i programmi mostrano una manina o altra simbologia che esprime una azione, mentre i file hanno l'aspetto di un documento.


Il sistema operativo Macintosh perfezionò ulteriormente il concetto: il nome del file aveva valore descrittivo, mentre altre informazioni, invisibili all'utente, codificavano il tipo (4 byte) e l'applicazione "creatrice" (altri 4 byte). La gestione di questi codici era completamente automatica, sicchè l'utente poteva assegnare ai suoi documenti nomi descrittivi arbitrari senza porsi problemi di "tipo" o "estensione": a tutti questi dettagli tecnici pensava l'interfaccia del sistema operativo.

Oggi esistono centinaia di formati file, e le estensioni corrispondenti cominciano a diventare astruse combinazioni di caratteri. Le estensioni a tre caratteri sono troppo limitate per garantire la leggibilità e l'unicità del tipo. Inoltre non sempre il trasferimento di un file richiede di inviare il nome del file stesso, sicché bisogna trovare un altro modo per trasmettere l'informazione del tipo che non sia legato al nome del file.

Con l'affermarsi di Internet per lo scambio di documenti si pose il problema di come codificare in modo universale il tipo di un file. La serie di documenti RFC 2045-2048 propose un meccanismo di denominazione dei tipi che ha la forma TIPO/SOTTOTIPO detto in breve "tipo MIME". Ad esempio, image/gif identifica una immagine nel formato GIF mentre text/html è una pagina WEB. Questa convenzione è stata adottata per l'email e anche per il protocollo del WEB e permette di identificare in modo universale il tipo di un file.

Le specifiche MIME e il protocollo HTTP permettono anche di inviare un nome di file. Addirittura il mittente può fare la distinzione tra il nome originale del file e il nome suggerito al destinatario. In altri termini, il mittente potrebbe inviare il nome originario per cortesia, ma anche il nome ripulito dagli artefatti legati alle convenzioni del suo specifico file system. Nel nostro discorso questi artefatti sono le estensioni che "inquinano" il nome descrittivo del file. Questo è un principio generale di Internet: il mittente fornisce le informazioni in un formato il più possibile universale, e il destinatario ricodifica queste informazioni passando dal formato universale al suo formato interno specifico.

Per quanto riguarda il nostro discorso sull'upload dei file, il browser invia insieme al file anche il suo nome proposto (non necessariamente corrispondente al nome usato nel file system del client) e il tipo MIME. Il server ha quindi tutte le informazioni necessarie per identificare il file e attribuirgli il nome appropriato in base alle proprie convenzioni per la gestione dei tipi.

C'è solo un caso nel quale lato server può essere necessario interessarsi della questione delle estensioni, e cioè quando il client fornisce il tipo MIME application/octet-string che individua un file binario generico. Questo potrebbe capitare, ad esempio, quando l'utente fa l'upload di un file da una macchina che non è quella dove il file è stato prodotto. In questo caso il server potrebbe (ma non è tenuto a farlo) analizzare l'eventuale estensione nel tentativo di dedurre un tipo MIME più specifico. Se il server riconosce nel nome del file una estensione nota, allora potrebbe suggerire all'utente il tipo MIME corretto. Sia però bene inteso che tutto questo è solo per venire incontro a una carenza che sta lato client, non lato server. L'onere di indicare il tipo MIME corretto spetta comunque al client.

Avvalersi dell'informazione sul tipo MIME comporta molti vantaggi: semplifica il lavoro lato server (non dobbiamo impegolarci con la questione delle estensioni); rende l'interfaccia più semplice e consistente, demandando al server ciò che è di sua competenza, e al client ciò che è del client; rende il programma più longevo (funzionerà anche con MPEG9); rende il programma indipendente dal file system del server e del client; rende gli utenti più contenti perché incontreranno meno problemi; è conforme ai dettami del protocollo HTTP, SMTP e di Internet in generale.

Come il programma PHP riceve il file

Il processo di upload di un file è piuttosto articolato perché molte cose possono andare storte. Le informazioni che il browser invia sono: il nome proposto per il file, il suo tipo MIME, il contenuto binario del file. Esaminiamo queste informazioni nel dettaglio.

Il nome proposto per il file. Di solito coincide con il nome del file sul file system del client, ma non è detto che debba essere così. Questo nome viene ritornato in $_FILES['miofile']['name'] sotto forma di stringa. La stringa è codificata nel charset della pagina che conteneva il FORM dal quale il file è stato inviato. Ovviamente al posto di 'miofile' metteremo il nome che abbiamo dato al control <input type=file name=miofile>.

Il tipo MIME del file. Viene ritornato in $_FILES['miofile']['type'] come stringa.

Il contenuto del file. Il file può avere una lunghezza arbitraria a partire da zero byte in su. Esiste un limite alla dimensione massima del file caricato che si può impostare dal file di configurazione php.ini oppure nel WEB server, come abbiamo già detto.

Il PHP salva il file ricevuto in un file temporaneo il cui nome viene messo in $_FILES['miofile']['tmp_name']. Questo file temporaneo viene automaticamente cancellato dal PHP una volta che il programma è terminato, per cui il programma deve avere cura di copiarlo in un altro file oppure cambiargli il nome prima di terminare.

Se l'utente non seleziona alcun file, il browser non dovrebbe inviare informazioni relative al suo control HTML. In realtà i browser che ho provato (Mozilla, MSIE) inviano un file vuoto senza nome e tipo MIME application/octet-stream. Il PHP associa a questo upload fittizio il codice di errore UPLOAD_ERR_NO_FILE.

Il PHP aggiunge poi altre due informazioni importanti:

La lunghezza del file. Viene ritornata in $_FILES['miofile']['size'] come numero intero a 32 bit in complemento a due. In pratica, si tratta di un numero compreso tra 0 e 2 GB. Correntemente PHP non supporta file più lunghi di 2 GB. Nonostante la grande velocità delle linee di comunicazione moderne, è improbabile che si possano gestire in modo efficace file monolitici così grandi attraverso il protocollo HTTP, per cui in pratica questo non è un vero limite.

Il codice di errore. Il campo $_FILES['miofile']['error'] contiene l'esito dell'operazione. Il valore UPLOAD_ERR_OK indica che tutto è andato bene.

Particolarità sul nome originale del file

Codifica del nome file. Il file system del client salva i nomi dei file usando un qualche charset. Tutti i sistemi operativi Microsoft a partire da Windows NT 4 usano l'Unicode per i nomi dei file per supportare tutti i sistemi di scrittura del mondo, e quindi per accontentare tutti gli utenti domestici che amano usare la propria lingua madre per denominare i loro documenti. Anche sui sistemi Unix e Linux è ormai comune usare l'Unicode, per l'esattezza la sua codifica di interscambio più comune nota come UTF-8. Le cose però non vanno sempre così, e la nostra WEB-application deve essere pronta ad ogni evenienza.

Quando il browser deve inviare il nome del file al server, il browser deve conoscere la codifica usata dal file system client e quindi deve convertire il nome del file nella nello stesso charset e nella stessa codifica della pagina che contiene il FORM. Ne consegue che la codifica più generale per le nostre pagine WEB è l'UTF-8.

Se invece la nostra pagina del FORM usa un charset meno generale, e il nome del file contiene dei caratteri più esotici di quelli previsti da questo charset, allora il browser si trova in un guaio e deve decidere come trattare i caratteri non rappresentabili nel charset della pagina. Ad esempio, se la nostra pagina del FORM è codificata in ISO-8859-1 e l'utente abita in un paese dell'est, probabilmente i nomi dei suoi file contengono un qualche carattere cirillico non compreso nel charset ISO-8859-1. Correntemente i browser sostituiscono il carattere in questione con la sequenza &xxx; dove xxx è il codice decimale UNICODE del carattere. Purtroppo il carattere & stesso non viene codificato, sicchè lato server non esiste un modo certo per identificare il problema e ci dovremo tenere il nome del file così com'è.

Aggirare il bug di IE. Un altro problema curioso è legato a un bug tuttora presente in Microsoft Internet Explorer, che invia non il nome del file, ma tutto il pathname completo! Ad esempio C:\WINDOWS\Desktop\Documenti\foto piero\spiaggia.jpg. Il problema è duplice: il browser invia al server più informazioni di quante l'utente probabilmente si aspetta, ed inoltre il nostro programma riceve non il nome del file ma tutto il path. Pietosamente il PHP pone rimedio eliminando tutto fino all'ultimo carattere di back-slash, per cui in questo esempio il programma PHP riceve solo spiaggia.jpg. Purtroppo questa soluzione drastica viene applicata indipendentemente dal browser, sicchè esiste il rischio che nomi di file validi provenienti da altri sistemi vengano erroneamente amputati. Ad esempio da Unix potremmo fare l'upload del file Fattura 3\2006.pdf, ma il nostro programma riceverebbe solo 2006.pdf che non è esattamente la stessa cosa.
Per chi volesse disabilitare questa scelta drastica fatta dagli sviluppatori del PHP deve modificare il codice sorgente main/rfc1867.c verso la fine, e poi implementare in PHP uno schema alternativo di sanificazione del nome del file magari basato sul nome del browser. Il pezzo di codice incriminato è questo:

/* The \ check should technically be needed for win32 systems only where
 * it is a valid path separator. However, IE in all it's wisdom always sends
 * the full path of the file on the user's filesystem, which means that unless
 * the user does basename() they get a bogus file name. Until IE's user base drops 
 * to nill or problem is fixed this code must remain enabled for all systems.
 */
s = strrchr(filename, '\\');
if ((tmp = strrchr(filename, '/')) > s) {
    s = tmp;
}

Notare che lo stesso trattamento fatto sul carattere di back-slash \ viene riservato per par condicio anche al carattere di slash /. Morale della fiaba: in upload il PHP tronca il nome del file al primo slash o back-slash che trova partendo da destra verso sinistra. In altri termini in PHP i caratteri slash e back-slash nei nomi dei file caricati non si possono usare. Se vogliamo disabilitare questo comportamento basta sostituire il codice di prima con questa riga e ricompilare:

s = filename;

Fatta questa modifica, dobbiamo ricordarci che quando riceviamo un file dal browser "MSIE" dobbiamo eliminare il path indesiderato.

Limitare la dimensione dei file in upload

Limitare la dimensione dei file inviati al server ha due scopi: salvaguardare la banda e lo spazio disco del server e offrire una interfaccia utente che impone delle restrizioni. Abbiamo già visto che il primo scopo lo si raggiunge configurando il WEB server e il php.ini. A livello del WEB server possiamo limitare la dimensione massima di un POST HTTP valido per tutto il nostro sito WEB. A livello del php.ini possiamo limitare ulteriormente la dimensione del POST e dei singoli file per tutti i programmi PHP del nostro sito WEB. Vediamo quali sono i meccanismi disponibili per impostare un limite a livello di singolo programma PHP e di singolo control di input file.

L'RFC 1867 propone l'inserimento dell'attributo MAXLENGHT nel control per l'input del file. Questo attributo permette di indicare la dimensione massima del file ricevibile dal server. Il browser dovrebbe quindi avvisare l'utente quando sta per inviare un file di dimensione eccessiva, senza che l'utente sprechi tempo e banda di rete inutilmente. Questo attributo sarebbe molto utile per costruire una interfaccia consistente, ma purtroppo nessun browser risulta supportarlo. Ho comunque inserito questo attributo nel nostro FORM di esempio nella eventualità che un giorno i browser lo implementino. Notiamo anche che questo attributo è falsificabile dal client, e quindi da solo non garantisce che i file inviati rispettino il limite imposto; si tratta solo di un ausilio di interfaccia. Lato server dovremo comunque adottare i soliti provvedimenti.

Il PHP propone invece di inserire un campo hidden MAX_FILE_SIZE *prima* del control di input file. Questo campo hidden specifica la dimensione massima in B del file che il nostro programma si aspetta. Contrariamente a quello che lascia pensare il manuale ufficiale del PHP, questo campo non ha alcun significato particolare per i browser; come tutti i campi hidden, esso viene inviato al programma indicato nell'action, ed è solo qui che viene interpretato dal PHP. Il PHP analizza il POST nell'ordine in cui arriva. Questo spiega anche perché questo campo hidden deve apparire prima del control di input del file: i browser generalmente inviano i valori dei control nello stesso ordine nel quale appaiono nel FORM, sicché l'interprete PHP può leggerne il valore prima che comincino i dati del file. Se il FORM contiene due o più control per input file, allora potremo specificare un valore diverso MAX_FILE_SIZE per ciascuno.

Il WEB server invia i dati del POST all'interprete PHP attraverso uno stream di bytes. A sua volta, PHP inizia a decodificare uno per uno i campi del FORM. Appena iniziano i dati del file, il PHP crea il file temporaneo e incomincia a salvarci dentro i dati man mano che arrivano dal WEB server. Se abbiamo inserito il control hidden MAX_FILE_SIZE nel FORM ma i dati del POST che compongono il file superano il massimo previsto, allora PHP interrompe il salvataggio sul disco (risparmiando spazio) e ritorna l'errore UPLOAD_ERR_FORM_SIZE; il resto del file contenuto nel POST viene ignorato, ma gli eventuali ulteriori campi del POST vengono regolarmente acquisiti. L'utilità di questo meccanismo è modesta e può essere aggirato facilmente da un utente malizioso. Inoltre non implementa una interfaccia verso l'utente. Visto che comunque i vari controlli li dobbiamo fare nel nostro programma, tanto vale lasciar perdere questo meccanismo, fare il solito doveroso controllo sulla dimensione del file, e presentare all'utente una adeguata interfaccia con messaggi di errore chiari.

In definitiva, a parte i meccanismi di protezione lato server che sono sempre necessari, non esiste un meccanismo di interfaccia predefinito che aiuti l'utente a rispettare una data dimensione massima per i file che invia. E' bene allora indicare esplicitamente il limite che abbiamo imposto nella stessa maschera del FORM, e dar modo all'utente di regolarsi di conseguenza senza rimanere frustrato da comportamenti inattesi della nostra applicazione WEB. Il programma di test che viene presentato in fondo a questo articolo adotta questa soluzione di interfaccia; la dimensione massima del file viene determinata dal programma dinamicamente in base alla configurazione corrente.

Acquisizione e validazione del file ricevuto

Per fissare le idee, svilupperemo una funzione di acquisizione e validazione del file caricato. Questa funzione riceve come argomento il nome del control del file, e restituisce come risultato finale un oggetto che descrive il file. La funzione restituisce NULL se il file non è stato caricato. Supporremo che il nostro programma lavori in UTF-8 sfruttando l'estensione MBSTRING del PHP. Per prima vediamo la struttura dell'oggetto ritornato:

<?php
/*******************************
    FileUpload.php
********************************/

/*.
    require_module 'standard';
    require_module 'preg';
    require_module 'mbstring';
.*/

class UploadedFile {
    public $error     = UPLOAD_ERR_OK;
    public $error_msg = "";
    public $name      = "";
    public $type      = "";
    public $size      = 0;
    public $tmp_name  = "";
}

In sostanza, i campi della classe UploadedFile riflettono i dati contenuti in $_FILE[] con l'unica eccezione del campo addizionale $error_msg.

Il campo $error dovrebbe contenere il valore UPLOAD_ERR_OK che indica tutto OK. Altrimenti il campo $error_msg contiene una descrizione in prosa del problema riscontrato. In caso di errore i dati del file vengono cancellati, e quindi non possiamo recuperare neanche parzialmente la porzione di file ricevuta prima che l'errore fosse rilevato.

Se l'upload è andato bene, il campo $name contiene il nome originale del file codificato nel charset UTF-8. E' possibile che in conseguenza delle operazioni di sanificazione la stringa risultante sia la stringa vuota "", nel qual caso il programma che usa la nostra funzione deve decidere come comportarsi.

Il campo $type contiene una stringa di caratteri ASCII stampabili (codici da 31 a 127). Si tratta di un dato fornito dal client, e quindi non affidabile, cioè la stringa ritornata potrebbe essere arbitrara e potrebbe non corrispondere a un tipo MIME valido e il carattere / potrebbe anche non essere presente.

La dimensione del file viene ritornata in $size come numero di bytes, un numero intero compreso tra 0 e 2^31-1.

Il campo $tmp_name è il nome del file temporaneo, completo di path, dove il PHP ha salvato il file. Il chiamante deve copiare o rinominare questo file, perché il PHP cancella automaticamente il file al termine del programma.

Vediamo ora la funzione. Ha due argomenti: il nome del control e l'elenco dei tipi MIME ammessi. Se accettiamo qualsiasi tipo MIME, allora basta passare l'array vuoto array(). La funzione restituisce NULL se il client non ha inviato il file o c'è qualche insolito problema di comunicazione o formato, altrimenti restituisce un oggetto UploadedFile. Tutti gli errori vengono loggati con trigger_error(), per cui sarà bene configurare php.ini in modo che questi errori vadano nel file di log del server e non al client. La descrizione dell'errore viene ritornata nel campo error_msg, e sarà questo il messaggio che potremo usare per dare retroazione all'utente. Ecco il codice:


/*. UploadedFile .*/ function get_uploaded_file(
    /*. string .*/ $name,
    /*. array[int]string .*/ $accept_types
)
{
    if( ! isset($_FILES) || ! isset($_FILES[$name]) )
        return NULL;

    $f = new UploadedFile();
    $f->error    = (int)    $_FILES[$name]['error'];
    $f->name     = (string) $_FILES[$name]['name'];
    $f->type     = (string) $_FILES[$name]['type'];
    $f->size     = (int)    $_FILES[$name]['size'];
    $f->tmp_name = (string) $_FILES[$name]['tmp_name'];

    switch( $f->error ){

    case UPLOAD_ERR_OK:
        break;
    
    case UPLOAD_ERR_INI_SIZE:
        $f->error_msg = "File troppo grande.";
        trigger_error("file too big (" . $f->size
            . " B) -- check upload_max_filesize in php.ini");
        return $f;
    
    case UPLOAD_ERR_FORM_SIZE:
        $f->error_msg = "File troppo grande.";
        trigger_error("file too big (" . $f->size
            . " B) -- check hidden field MAX_FILE_SIZE");
        return $f;

    case UPLOAD_ERR_PARTIAL:
        $f->error_msg = "Problema di comunicazione; "
            . "solo parte del file pervenuto.";
        trigger_error("partial upload");
        return $f;
    
    case UPLOAD_ERR_NO_FILE:
        # Nessun file caricato o nome di file mancante.
        return NULL;

    case UPLOAD_ERR_NO_TMP_DIR:
        $f->error_msg = "Problema di configurazione sul server.";
        trigger_error("file upload: missing tmp dir.");
        return $f;

    case UPLOAD_ERR_CANT_WRITE:
        $f->error_msg = "Problema di configurazione sul server.";
        trigger_error("file upload: can't write file "
        . $f->tmp_name);
        return $f;

    case UPLOAD_ERR_EXTENSION:
        $f->error_msg = "Upload interrotto.";
        trigger_error("file upload: interrupted by extension");
        return $f;

    /*
        Meglio prevedere anche errori imprevisti realmente...
        imprevisti!
    */
    default:
        $f->error_msg =
            "Errore non previsto durante il caricamento del file.";
        trigger_error("file upload: unexpected error code "
        . $f->error);
        return $f;
    }

    /*
        Siccome siamo paranoici, controlliamo anche che il $size
        sia positivo o nullo:
    */
    if( $f->size < 0 ){
        trigger_error("file upload: size " . $f->size);
        return NULL;
    }

    /*
        ...e che il campo $size corrisponda alla dimensione del file
        temporaneo:
    */
    if( $f->size != filesize($f->tmp_name) ){
        trigger_error("file upload: claimed size " . $f->size
            . ", actual tmp file size " . filesize($f->tmp_name));
        return NULL;
    }

    /*
        Controllo sanitario sul nome del file. Forziamo il rispetto
        della codifica UTF-8 eseguendo una conversione da UTF-8 in
        se' stesso, poi eliminiamo i codici di controllo da 0 a 31
        e il 127 (DEL).
        ATTENZIONE! il nome di file risultante potrebbe anche essere
        la stringa vuota.
    */
    $f->name = mb_convert_encoding($f->name, 'UTF-8', 'UTF-8');
    $f->name = preg_replace("/[\\000-\\037\\177]/", "", $f->name);

    /*
        Controllo sanitario sul tipo MIME. Eliminiamo tutti i codici
        ASCII eccetto quelli stampabili da 33 a 126. Il MIME type
        risultante deve contenere un solo carattere "/" e almeno un
        carattere a sinistra e a destra di esso, altrimenti da'
        errore e ritorna NULL.
    */
    $f->type = preg_replace("/[\\000-\\037\\177-\\377]/", "",
        $f->type);
    $l = strlen($f->type);
    $x = (int) strpos($f->type, "/"); # da' 0 se "/" manca
    $y = (int) strrpos($f->type, "/"); # da' 0 se "/" manca
    if( $l == 0 || $x != $y || $x == 0 || $x == $l-1 ){
        trigger_error("file upload: empty or invalid MIME type "
        . $f->type);
        return NULL;
    }

    if( count($accept_types) > 0
    and ! in_array($f->type, $accept_types) ){
        $f->error = 1111;  # FIXME: codice errore non standard
        $f->error_msg = "Il file di tipo " . $f->type
            . " non &egrave; tra quelli ammessi: "
            . implode(", ", $accept_types) . ".";
    }

    return $f;
}
?>

Il chiamante di questa funzione deve controllare che il valore di ritorno non sia NULL, che il codice di errore sia E_UPLOAD_ERR_OK, che il nome del file non sia vuoto o troppo lungo, che il tipo MIME non sia vuoto o troppo lungo, e che la lunghezza del file sia nei limiti previsti, e quindi deve dare una adeguata retroazione all'utente se qualcosa è andato storto:

$f = get_uploaded_file("FOTO",
    array("image/gif", "image/png", "image/jpeg", "image/pjpeg"));
$err = "";
if( $f === NULL ){
    $err = "File non indicato.";
} else if( $f->error != UPLOAD_ERR_OK ){
    $err = $f->error_msg;
} else if( $f->size > 512000 ){
    $err = "File troppo grande, previsti al massimo 500 KB.";
} else {
    if( strlen($f->name) == 0 ){
        $f->name = "SENZA-NOME-" + rand();
    }
    if( strlen($f->type) == 0 ){
        $f->type = "application/octet-stream";
    }
}

if( strlen($err) == 0 ){
    # Qui: salvare il file $f->tmp_name con nome $f->name
    # e tipo MIME $f->type
    echo "Upload riuscito.";
} else {
    echo "Upload non riuscito: $err";
}

Dove mettere i file caricati?

Qui consideriamo tre possibili soluzioni che fanno uso del solo file system, del file system e del DB, e del solo DB. Esaminiamo i pro e i contro di ciascun metodo.

Salvare i file nel file system. E' cosa insana assolutamente da evitare. Ogni server ha un suo file system con sue particolari proprietà e limitazioni. Le limitazioni riguardano: lunghezza massima dei nomi dei file; encoding dei nomi dei file; caratteri vietati; nomi speciali riservati; sensibilità alla differenza tra maiuscole e minuscole.

Vediamo per esempio la situazione nei sistemi Unix e similari. La lunghezza massima dei nomi varia da 14 nei sistemi più vecchi a 255 byte nei sistemi più recenti. Non esiste il concetto di encoding dei nomi dei file: il nome di un file è una qualsiasi sequenza di byte, sono vietati solo lo slash / (che separa gli elementi del path) e il byte nullo (che marca la fine del nome). I nomi di file . e .. sono riservati. Lettere maiuscole e minuscole sono distinte, per cui Pippo, PIPPO e pippo sono nomi distinti.

Altri problemi da considerare:

Ciò detto, se proprio vogliamo salvare i file sul file system del server, dovremo codificare il nome opportunamente. L'articolo PHP e caratteri esotici (www.icosaedro.it/articoli/php-i18n.html) discute del problema della codifica dei nomi dei file da e per UTF-8. Ricordarsi però che in Windows nomi diversi che differiscono solo per lettere maiuscole e minuscole sono considerati uguali. Ricordare inoltre che ci sono sempre delle limitazioni al numero massimo di file per directory e alla lunghezza massima del file. Da controllare anche che il nome non contenga dei caratteri slash (Unix, Linux, Windows) o dei caratteri back-slash (Windows) che il sistema operativo interpreta come separatori di directory. Infine, certi nomi di file sono speciali e non si possono usare, come punto (directory corrente) e punto,punto (directory genitrice). La funzione move_uploaded_file() può aiutare ad evitare guai.

I dolori nascono se poi vogliamo mostrare all'utente il contenuto della directory dove abbiamo salvato i file caricati. A questo scopo useremo la funzione dir() che ritorna un oggetto Directory, e il metodo read() di questo oggetto ritorna la prossima entrata della directory. Sfortunatamente, read() non funziona su Windows quando il nome del file contiene caratteri superiori al set ISO-8859-1 (vedi bug #35300, http://bugs.php.net/bug.php?id=35300). La soluzione del problema sembra rimandata a PHP 6.

Salvare i file nel file system e i meta-dati nel DB. Questa è di gran lunga la soluzione preferibile. Le meta-informazioni vanno in una tabella del DB, mentre la chiave primaria PK viene usata anche come nome del file sotto il quale salvare il contenuto del file. In generale un PK è un banale numero intero. Nel DB potremmo salvare molte altre informazioni utili, per esempio: nome originale del file codificato UTF-8, tipo MIME, size, utente che lo ha inviato, categoria, data di upload, data di scadenza o tempo di senescenza se non interessa a nessuno, permessi di accesso, eventuale commento che lo descrive, numero di download, indice di popolarità o interesse, e quant'altro possa servire. Appoggiandosi a un DB si possono implementare funzionalità molto più potenti di quelle disponibili in un qualsiasi file system, ed inoltre si realizza l'indipendenza dal file system specifico sottostante.

Non mettere i file in un'unica directory, ma sparsi in un albero in modo che ogni directory contenga al massimo poche decine di files. Il criterio di ordinamento potrebbe essere quello della PK. Esempio: se la PK è 123456789, il file va messo in $FS_BASE/12/34/56/78/9. Questo permette di ottenere il path del file data la PK e viceversa.

Ricordare che la directory $FS_BASE dove mettiamo i file deve essere irraggiungibile da WEB, cioè deve essere fuori della DocumentRoot di Apache. L'accesso degli utenti ai file caricati non deve mai essere diretto, ma disciplinato dal nostro programma.

La memoria RAM del server non viene sollecitata: per salvare il file dovremo solo rinominarlo, mentre per inviare il file al client basterà fare un readfile($path); (l'articolo PHP File Download spiega meglio questo punto).

Se i file devono essere ordinati gerarchicamente, creare una gerarchia virtuale di directory mantenuta dal DB da presentare all'utente. Questa gerarchia di directory non ha nulla a che vedere con la distribuzione dei file nel file system del server.

Usare solo il DB. A volte salvare i file nel file system non è possibile o non è conveniente. Spesso questo dipende da limitazioni imposte dall'ISP presso il quale il nostro sito WEB viene ospitato. Ecco alcuni motivi per i quali diventa necessario ricorrere al DB:

In casi come questi dovremo utilizzare gli speciali campi del DB noti come BLOB (Binary Large OBjects) che possono prendere nomi diversi nei diversi DBMS (BYTEA in PostgreSQL oppure LONGBLOB in MySQL). Questi campi del DB possono contenere fino a 4 GB, ma attenzione che il loro contenuto deve transitare per intero nella memoria del programma sia in scrittura che in lettura, per cui il vero limite alla dimensione dei file è il parametro di configurazione memory_limit del php.ini.

Vediamo ad esempio i comandi PostgreSQL per creare una tabella di file, inserire un file e poi rileggerlo:

CREATE TABLE files (
    pk SERIAL,
    name TEXT,
    type TEXT,
    content BYTEA );
==> creating table `files'
==> creating sequence `files_pk_seq'

INSERT INTO files (
    name,
    type,
    content)
VALUES (
    'prova.jpg',
    'image/jpeg',
    decode('MTIzNDU2Nzg5MA==','base64'));

SELECT currval('files_pk_seq');
==> 123456

SELECT
    name,
    type,
    encode(content, 'base64') as content_base64
FROM files
WHERE pk=123456;
==> [ RECORD 1 ]
==> name = 'prova.jpg'
==> type = 'image/jpeg'
==> content_base64 = 'MTIzNDU2Nzg5MA=='

In questi esempi ho usato la codifica Base64 per il dialogo tra il PHP e il DBMS, in modo che il contenuto del file viaggi codificato come stringa di testo Base64 e restituito nella stessa codifica: questa è la codifica più compatta rispetto a quella "escaped" classica e a quella esadecimale, che sono le altre due alternative. Il file verrà codificato con qualcosa del tipo base64_encode(file_get_content($file)) e poi decodificato e inviato al client con qualcosa del tipo echo base64_decode(pg_fetch_result($res, 0, "content_base64"));.

Comunque sia, per questa manipolazione a livello di stringhe del contenuto del file prova.jpg dobbiamo prepararci a vedere l'interprete PHP sprecare parecchia memoria sul server, pari a tre o quattro volte la dimensione del file, ma se non si sta attenti a come si scrive il programma si sprecherà ancora di più.

Un modo più efficiente per sfruttare i campi BLOB è quello di usare le apposite funzioni di accesso di PostgreSQL della serie pg_lo_*(): il programma tende a complicarsi un po', ma la memoria del server viene meno sollecitata. Per MySQL c'è l'equivalente metodo send_long_data() della estensione MySQL Improved.

La barra di avanzamento

Una mancanza spesso sentita dei browser odierni è una progress bar che indichi il grado di avanzamento dell'upload di un file. Questa retroazione di interfaccia sarebbe utile all'utente per rendersi conto che il computer sta lavorando e che deve semplicemente aspettare, e sarebbe particolarmente gradita durante l'upload di file molto grandi. L'utente avrebbe anche la possibilità di annullare il trasferimento se l'operazione appare richiedere un tempo spropositato rispetto alle aspettative. Visto che i browser già prevedono una progress bar per il download, mancherebbe solo la sua controparte per l'upload. Io credo che questa lacuna sarà presto colmata dagli sviluppatori.

Nel frattempo alcuni sviluppatori di applicazioni WEB tentano di aggirare questa mancanza con soluzioni alternative, spesso basate su routine già predisposte basate su JavaScript, o AJAX o Applet. Lato client, la routine esegue il polling sul server per aggiornare la barra di avanzamento. Questo naturalmente prevede che lato server sia a sua volta predisposta una funzionalità in grado di rispondere a tali richieste. Nel caso del PHP dovremo installare una apposita estensione come uploadprogress dal sito PECL (www.pecl.php.net/package/uploadprogress).

Esistono tuttavia alcune controindicazioni a tali soluzioni:

In definitiva io credo sia meglio evitare queste soluzioni perché tendono a creare più problemi di quanti ne risolvano. Inoltre la presenza della progress bar per il download fa sperare che presto i browser implementeranno anche la sua controparte in upload, colmando la lacuna.

Programma di test dell'upload

Ho realizzato un piccolo programma per i test dell'upload dei file:

php-file-upload-test.cgi

Il programma permette di caricare un file e vedere come viene istanziato l'array $_FILES[] e altre variabili di sistema. Dall'interno del programma stesso si può scaricare il suo sorgente (c'è un'ancora in fondo alla pagina).

Come continuare

L'articolo PHP File Download (www.icosaedro.it/articoli/php-file-download.html) completa questa mini-serie di due articoli esaminando il problema opposto dell'upload: il download dei file. Anche lì parecchie sorprese dovute al diverso comportamento dei vari browser.

Bibliografia

Argomenti correlati


Umberto Salsi
Commenti
Contatto
Mappa
Home / Indice sezione
An abstract of the lastest comments from the visitors of this page follows. Please, use the Comments link above to read all the messages or to add your contribute.

2015-04-09 by Guest
Re: Domande da neofita.
Anonymous wrote: [...] L'entrata "error" dice tutto, come infatti risulta dal manuale: UPLOAD_ERR_FORM_SIZE Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. Togli MAX_FILE_SIZE dal form o aumenta il suo valore. [more...]

2015-04-08 by Guest
Re: Domande da neofita.
... chiedo scusa, i campi si chiamano IF_1_AUDIO_F, IF_2_AUDIO_F e IF_3_AUDIO_F... Anonymous wrote: [...] (PLEASE ADD YOUR COMMENTS HERE) [more...]

2015-04-08 by Guest
Domande da neofita.
Salve, ho un problema che sembra irrisolvibile: la mia form contiene, tra le altre cose, tre campi di tipo "file" (IF_1_AUDIO, IF_2_AUDIO, IF_3_AUDIO) e chiamo un file.php che banalmente "dovrebbe" ciclare sui tre file per fare l'upload ma la superglobal $_FILES mi risulta sempre cosi' fatta: array(3) { ["IF_1_AUDIO_F"]=> array(5) { ["name"]=> string(20) "Ululato_del_lupo.mp3" ["type"]=> string(0) "" ["tmp_name"]=> string(0) "" ["error"]=> int(2) ["size"]=> int(0) } ["IF_2_AUDIO_F"]=> array(5) { ["name"]=> string(0) "" ["type"]=> string(0) "" ["tmp_name"]=> string(0) "" ["error"]=> int(4) ["size"]=> int(0) } ["IF_3_AUDIO_F"]=> array(5) { ["name"]=> string(0) "" ["type"]=> string(0) "" ["tmp_name"]=> string(0) "" ["error"]=> int(4) ["size"]=> int(0) } } Ho inserito un solo file e quindi gli altri tre elementi dell'array dovrebbero essere, giustamente, vuoti ma anche del primo non mi da' i valori che mi servono (size, tipo, tmp_name) Cosa puo' essere? Il file in questione, un mp3, e' di[...][more...]

2008-03-26 by Umberto Salsi
Re: Upload file molto grandi
Anonymous wrote: [...] Evidentemente il php.ini che hai modificato non è quello che viene poi letto dal tuo programma PHP. Fatti stampare da phpinfo() qual'è il vero php.ini che viene usato e vai a modificare quello. [more...]

2008-03-25 by Guest
Re: Upload file molto grandi
Anonymous wrote: [...] (PLEASE ADD YOUR COMMENTS HERE) Dimenticavo di aggiungere che la dimensione dei file da uploadare è di circa 50 Mb e che l'operazione viene svolta solo da un utente autenticato. Daniele [more...]

2008-03-25 by Guest
Upload file molto grandi
Buongiorno, innanzi tutto complimenti per la pulizia del codice e la chiarezza nello spiegarlo! Lo script funziona perfettamente in locale, ma fino ad una certa dimensione (circa 16 Mb). Ho provato a modificare il file php.ini, ma anche immettendo valori molto grandi per max-filesize e post-max-size il risultato è lo stesso. Testando php.ini con il suo script (bellissimo!) vedo effettivamente che post-max-size ha un valore di 16Mb, mentre upload-max-size di 32Mb, oltretutto appare un avviso che post dovrebbe essere maggiore di max... Perché i valori che ho impostato in php.ini non vengono accettati? Grazie mille per l'aiuto. Daniele[more...]

2008-01-29 by Umberto Salsi
Re: Grazie
Anonymous wrote: [...] E' giusto precisare che gli esempi di codice dell'articolo si riferiscono al PHP 5. Comunque ormai il PHP 4 non si dovrebbe più usare e il codice nuovo dovrebbe essere sempre PHP 5. [...] Sì, l'esempio che proponi è il modo "basilare" di implementare l'upload dei files in PHP, ma il mio articolo si propone proprio di considerare tutti gli aspetti di sicurezza e di integrità senza lasciarli al caso. Ad esempio - manca una diagnostica degli errori che dia all'utente un messaggio comprensibile se qualcosa è andato storto; - manca una diagnostica sull'encodig del nome del file; - move_uploaded_file() rischia di sovrascrivere file arbitrari, magari un file già precedentemente uploadato o addirittura un sorgente PHP (a meno che il safe_mode e l'open_basedir non siano settati per bene, ma poi bisogna aggirare le limitazioni che questi settaggi introducono, mentre il mio intento è presentare un codice indipendente da particolari settaggi di php.ini); - salvare i file nel[...][more...]

2008-01-24 by Guest
Re: Grazie
Articolo ben fatto ma bisogna specificare che lo script è valido solo per php 5.x su php 4.x.x la sintassi è ben diversa e pertanto viene generata una serie di errori es. Parse error: syntax error, unexpected T_CONST, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' .......on line 32 .....e via dicendo Molto più semplicemente (si puo rendere più complesso con una serie di controlli aggiuntivi): 1 file upload.htm <html> <body> <form method="post" action="upload.php" enctype="multipart/form-data"> <input type="file" name="FOTO"> <input type="submit" value="Upload"> </form> </body> </html> 2 file upload.php <?PHP $cartella = 'upload/'; $percorso = $_FILES['FOTO']['tmp_name']; $nome = $_FILES['FOTO']['name']; if (move_uploaded_file($percorso, $cartella . $nome)) { print "Upload eseguito con successo"; } else { print "Si sono verificati dei problemi durante l'Upload"; } ?> Ciao Agostino [more...]

2008-01-18 by Guest
Grazie
La ringrazio tantissimo delle preziose informazioni contenute in questo sito, senza persone come lei ci sarebbe molta più ignoranza nel mondo dell'informatica. Nicolò Scarpa[more...]