Home / Indice sezione | www.icosaedro.it | ![]() |
Ultimo aggiornamento: 2018-08-03
In questo articolo esaminiamo il processo di download dei file via protocollo HTTP, cioè il trasferimento dal server al client. Vedremo in particolare come realizzare il download dei file, come gestire nomi di file contenenti caratteri diversi dall'ASCII, come gestire i problemi di compatibilità tra i vari browser, come gestire il caching e le sessioni. Gli esempi dati sono per il popolare linguaggio di programmazione PHP. Questo articolo completa l'articolo PHP File Upload (www.icosaedro.it/articoli/php-file-upload.html).
Come presentare i file all'utente
Come permettere il download all'utente
La ricetta per gli impazienti
Come codificare il nome del file
Gestire caratteri speciali ed estensioni
Come fare in pratica
La trappola delle sessioni
Caching
Il programma di test
Bibliografia
I file hanno delle proprietà, che sono il nome, il tipo, la lunghezza, la data di aggiornamento e ogni altra informazione che possa tornare utile all'utente per scegliere consapevolmente se scaricare il file oppure no. Questo vale soprattutto per i file lunghi, che richiedono tempo e impegnano la rete e il server. E' nell'interesse del gestore del sito WEB assicurare che i file scaricati corrispondano alle aspettative degli utenti.
Il modo più generale per presentare il tipo di un file è usare il tipo MIME. Potremo anche associare una icona che richiama la natura del file, ma il tipo MIME è l'informazione più completa che possiamo dare. A livello di interfaccia torna utile fornire anche una descrizione in prosa, almeno per i tipi di file più comuni. Questa funzione serve proprio allo scopo:
/*. string .*/ function MIMEtoHumanReadable(/*. string .*/ $mime) { switch($mime) { case "text/plain": return "testo"; case "text/html": return "ipertesto"; case "image/gif": case "image/png": case "image/bmp: return "immagine per punti"; case "image/jpeg": return "immagine per punti di tipo fotografico"; case "application/postscript": return "documento PostScript"; case "application/msword": return "documento Microsoft Word"; case "application/zip": return "file compattato"; case "video/mpeg": case "video/quicktime": case "video/x-msvideo": return "filmato"; case "application/octet-string": return "file binario"; /* ecc. ecc. */ default: trigger_error(__FUNCTION__ . ": missing case for MIME type $mime"); return $mime; } }
Ovviamente sono possibili altre soluzioni più generali e più sofisticate che sfruttano un array, oppure una tabella del DB per consentire aggiornamenti senza modificare il programma, e che magari supportino la lingua preferita dal navigatore e l'icona associata al tipo.
Supponiamo di avere i dati di un file in altrettante variabili:
# Nome originale del file # (codifica UTF-8 di "Caffè Brillì.pdf"): $file_name = "Caff\xC3\xA8 Brill\xC3\xAC.pdf"; # Suo tipo MIME: $file_mime = "application/pdf"; # Collocazione effettiva del file sul server: $file_path = "/home/private_dir/01/23/45/06.dat";
Qui ho scritto il nome del file in caratteri ASCII perché non ci siano ambiguità di rappresentazione di questo documento; nella pratica usando un text editor moderno si possono creare direttamente le stringhe UTF-8 in PHP.
Per la sola visualizzazione basta indicare il tipo MIME nella intestazione HTTP e poi inviare il contenuto binario del file:
function echoFile($fn) { $f = fopen($fn, "rb"); if( $f === FALSE ) throw new RuntimeException("cannot read file $fn"); do { $chunk = fread($f, 4196); if( $chunk === NULL ) break; echo $chunk; } while(TRUE); fclose($f); } header("Content-Type: $file_mime"); echoFile($file_path);
La funzione readfile() non va bene per inviare il file
al client perché può causare un out of memory error se il file
è più grande dello spazio in memoria disponibile per lo script
(vedere PHP BUG
61636 per ulteriori info). Il rimedio proposto di disattivare
l'output buffering con:
while( ob_get_level() ) ob_end_flush(); readfile($fn);
funziona, ma per file lunghi intercorre un grande ritardo prima che il browser cominci a ricevere dati e proponga la dialog di salvataggio. Non rimane che scrivere una funzioncina come quella qui sopra per poter inviare file di arbitraria lunghezza e in modo indipendente dall'output buffering.
Quando il browser riceve questa risposta tenterà del suo meglio per visualizzare il file. Se il tipo MIME è tra quelli generalmente supportati nativamente dai browser (come testi puri e immagini GIF/PNG/JPEG) allora verrà visualizzato, altrimenti il browser può avvalersi anche di applicazioni esterne. Ad esempio, in Mozilla ci sono le configurazioni delle "Helper Applications" dove si possono aggiungere nuovi tipi MIME e rispettive applicazioni che li visualizzano.
Per suggerire il download come file piuttosto che la
sua visualizzazione, il server deve indicare che la risposta
contiene un "allegato". Questo richiede un ulteriore header
Content-Disposition
dove andremo a specificare anche il
nome originale del file:
header("Content-Type: $file_mime"); header("Content-Disposition: attachment; filename=\"$file_name\""); header("Content-Length: " . filesize($file_path)); echoFile($file_path);
Il parametro attachment
farà aprire la
finestra di dialogo per il salvataggio del file.
Il suo contrario è inline
che vuol dire: mostra
il documento ed eventualmente consenti di salvarlo.
Comunque sia, il parametro filename=...
suggerisce il nome
del file da usare nel caso che l'utente voglia salvare il file su disco.
L'intestazione Content-Length
permette al browser di
conoscere in anticipo la dimensione finale del file. Inoltre il browser
usa questo dato per mostrare la percentuale del download. Se il contenuto
del file deve essere generato dinamicamente e la lunghezza non si conosce
in anticipo, allora questa intestazione non va messa. Nei nostri esempi
usiamo dei file, quindi filesize()
ci darà il valore
richiesto.
Abbiamo trascurato di specificare come codificare il nome del file: cosa succede se il nome del file contiene le virgolette doppie? come fa il browser a sapere quale charset usa? e come si comporta in generale il browser se il nome del file risulta non valido sul file system del client? Queste sono le questioni che andremo a precisare nei prossimi paragrafi e sono il vero grande problema.
Browser diversi hanno comportamenti diversi quando il nome del file contiene dei caratteri non-ASCII. Questo spezzone di codice produce il download via HTTP di un file generico con nome codificato UTF-8:
$file_name = "Caffé Brillì.pdf"; # file name, UTF-8 encoded $file_mime = "application/pdf"; # MIME type $file_path = "absolute/or/relative/path/file"; header("Content-Type: $file_mime"); header("Content-Length: " . filesize($file_path)); $agent = isset($_SERVER["HTTP_USER_AGENT"])? $_SERVER["HTTP_USER_AGENT"] : ""; if( is_int(strpos($agent, "MSIE")) ){ // MS IE <= 6 # Remove reserved chars: :\/*?"<>| $fn = preg_replace('/[:\\x5c\\/*?"<>|]/', '_', $file_name); # Non-standard URL encoding: header("Content-Disposition: attachment; filename=" . rawurlencode($fn)); } else if( is_int(strpos($agent, "Gecko")) ){ # RFC 2231, 5987: header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($file_name)); } else if( is_int(strpos($agent, "Opera")) ) { # Remove reserved chars: :\/*{? $fn = preg_replace('/[:\\x5c\\/{?]/', '_', $file_name); # RFC 2231, 5987: header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($fn)); } else { # RFC 2616 ASCII-only encoding: $fn = mb_convert_encoding($file_name, "US-ASCII", "UTF-8"); $fn = (string) str_replace("\\", "\\\\", $fn); $fn = (string) str_replace("\"", "\\\"", $fn); header("Content-Disposition: attachment; filename=\"$fn\""); } echoFile($file_path);
Purtroppo non tutti i problemi sono risolti da questo spezzone di codice. Una soluzione più rifinita richiede di considerare anche altre particolarità e comportamenti strani dei vari browser e altre questioni ancora rimangono poco chiare, come il comportamento rispetto al conflitto tipo MIME/estensione e il comportamento rispetto ai caratteri riservati del file system del client.
Una implementazione delle funzioni di download
di file è disponibile nel package PHPLint, classe FileDownload;
la funzione sendHeaders() permette di inviare l'header, mentre
la funzione sendFile() permette di inviare il file.
Negli esempi più banali di prima abbiamo semplicemente inserito il nome del file nell'header HTTP ignorando del tutto il problema della codifica del nome del file. Questo funziona solo se il nome del file contiene solo codici ASCII. In generale, invece, il nome del file è una stringa arbitraria espressa in un certo charset. Per fissare le idee, nel seguito useremo sempre l'UTF-8, che permette di codificare praticamente tutti gli alfabeti del mondo. Passiamo in rassegna le soluzioni possibili.
L'RFC 2616 descrive il protocollo HTTP/1.1. In questo documento si
afferma che il valore del parametro filename deve essere codificato
ASCII, oppure in ISO-8859-1, oppure ancora secondo la codifica "MIME
header" del documento RFC 2047. Gli autori hanno trascurato di indicare
come il browser dovrebbe fare per determinare quale di queste codifiche
è stata usata, per cui una certa euristica si rende necessaria.
Senza entrare nei dettagli di tutti questi documenti, vediamo come le
diverse codifiche si applicano a un file di nome Caffè
Brillì.pdf
. Questo nome di file contiene due lettere
accentate e pertanto non è ASCII.
Codifica ASCII. Abbiamo due possibilità: eliminare i caratteri non ASCII o sostituirli con un qualche altro carattere ASCII come "?" oppure "_". Ovviamente questo altera il nome del file oppure lo rende del tutto illeggibile, soprattutto con le lingue dei paesi dell'est. Ancora, le virgolette doppie e il back-slash vanno preceduti da un back-slash. Ovviamente, tutti i browser supportano la codifica ASCII.
Codifica "raw". E' quella che abbiamo usato negli esempi
elementari di prima, inserendo direttamente il filename codificato UTF-8
dentro all'header HTTP. I caratteri virgolette doppie "
e back-slash \
, se presenti, devono essere fatti precedere
da un back-slash.
Qui sotto ho scritto la stringa risultante nel formato PHP, in modo che
sia chiaro byte per byte ciò che viene inviato al browser:
"Content-Disposition: attachment; filename=\"Caff\xC3\xA8 Brill\xC3\AC.pdf\""
Notare che la lettera "ì
" viene inviata come i due byte
"\xC3\xA8
" e qualcosa di simile succede per l'altra vocale
accentata. Questa è la codifica UTF-8. Il browser non ha modo
di capire quale codifica è stata usata. Secondo le specifiche
RFC 2616, rilevata la presenza di caratteri non-ASCII il browser dovrebbe
supporre la codifica ISO-8859-1, per cui il risultato visualizzato
dal client dovrebbe essere questo:
Caffè Brillì.pdf
che non è certo quello che volevamo!
Codifica ISO-8859-1. Trasformare la codifca del nome del file in ISO-8859-1 di solito non comporta problemi fintanto che ci limitiamo ai paesi occidentali, e potrebbe essere una soluzione adatta nella maggior parte dei casi, anche se non del tutto generale. Come prima, i caratteri non traducibili si possono eliminare o sostituire con un altro. Le virgolette doppie e il back-slash vanno preceduti da un back-slash.
Codifica MIME header. L'RFC 2616 rimanda all'RFC 2047 per una codifica alternativa all'ISO-8859-1. L'RFC 2047 prevede due meccanismi: il quoted-printable e il Base64. Quest'ultimo è facilmente implementabile senza ricorrere ad estensioni strane del PHP. Ecco come si presenta il nostro nome di file:
Content-Dispositon: attachment; filename="=?UTF-8?B?Q2FmZsOoIEJyaWxsw6wucGRm?="
Senza entrare nei dettagli di come funziona in generale questa codifica, nel
nostro caso si tratta solo di mettere la stringa codificata Base64 tra la
sequenza di caratteri "=?UTF-8?B?
" all'inizio e la sequenza
"?=
" alla fine.
L'RFC 2047 fissa in 75 byte la lunghezza delle stringhe così generate. Oltre questo limite bisogna scomporre il nome del file in pezzi più piccoli e codificarli separatamente. Questo almeno in teoria. Nella pratica Mozilla e Opera riconoscono senza problemi stringhe ben più lunghe, per cui non vale la pena di complicare il codice del programma.
Codifica enhanced. E' quella proposta dall'RFC 2231 e RFC 5987, che estende
il protocollo HTTP introducendo un nuovo parametro "filename*
"
al posto del vecchio "filename
" (notare la presenza di un
asterisco alla fine del nome del parametro). Lo schema di questa codifica
è molto semplice, e nel nostro caso il risultato sarebbe:
Content-Disposition: attachment; filename*=UTF-8''Caff%C3%A8%20Brill%C3%AC.pdf
Notiamo che il nome del charset usato viene messo in chiaro all'inizio, seguito da due virgolette singole, seguito dal nome del file URL-encoded. Questo formato è il più generale, non soffre di problemi di ambiguità ed è anche semplice da implementare. In breve: è il migliore.
Codifica UTF-8 URL-Encoded. Cercando in giro come risolvere con MSIE, ho scoperto che accetta il nome del file UTF-8 codificato come si fa con i parametri degli URL. Non ho trovato uno standard o altro documento ufficiale che lo descriva, ma questo è l'unico modo che ho trovato per inviare l'UTF-8 a MSIE versione 8 e precedenti.
Ho poi provato tutti gli algoritmi di codifica qui esaminati con il programma di test che viene citato al termine di questo articolo. Condizioni del test: il nome del file è quello degli esempi precedenti, codificato UTF-8. Si vuole verificare come si comportano i vari browser quando il nome del file contiene dei caratteri non-ASCII.
Browser/S.O. | Raw | MIME header | Enhanced | URL-Encoded |
---|---|---|---|---|
Chrome 31.0/Windows Vista | Sì | Sì | Sì | Sì |
MSIE 9.0/Windows Vista | No (3) | No | Sì | Sì (7) |
MSIE 8.0/Windows Vista | No (3) | No | No (6) | Sì (7) |
MSIE 6.0/Windows 98 | No (3) | No (4) | No (5) | Sì |
Firefox 25.0/Windows Vista | Sì | Sì | Sì | No |
Firefox 3.6/Windows Vista | Sì | Sì | Sì | No |
Firefox 3.6/Linux | Sì | Sì | Sì | No |
Mozilla 1.7.13/Linux | Sì (1) | Sì | Sì | No |
Opera 9.01/Linux | Sì (1) | No (2) | Sì | No |
Opera 9.01/Windows 98 | Sì (1) | No (2) | Sì | No |
Risultati dei test sul trasferimento del nome file codificato UTF-8.
NOTE:
1. Mozilla e Opera sembrano avere abbracciato la tendenza corrente in Internet di considerare l'UTF-8 come la naturale estensione dell'ASCII. Di conseguenza il charset default non è ISO-8859-1, ma UTF-8.
2. Opera non interpreta correttamente la codifica MIME header e mostra un laconico "
3. Presume la codifica ISO-8859-1, per cui i caratteri UTF-8 diventano casuali.
4. Mostra un nome generato casualmente, come "
CAPSSFDX
".5. Mostra il "basename" dell'URL, nel mio caso
php-file-download-test.cgi
.6. Mostra il "basename" dell'URL ma con estensione PDF, cioè
php-file-download-test.pdf
.7. Con la codifica non-standard URL-encoded, se l'estensione del nome del file manca, allora incomprensibilmente lo spazio codificato come %20 non viene riconvertito in spazio e rimane %20. Con questo browser è quindi sempre necessario fornire la corretta estensione.
Il compito del server è (dovrebbe essere...) consegnare al browser il nome del file esattamente così com'è, compresa la sua eventuale estensione. Se il sistema dove gira il browser adotta il meccanismo delle estensioni per assegnare un tipo ai file, allora sarà il browser a controllare la corrispondenza tra l'estensione e il tipo MIME: se l'estensione manca il browser l'aggiunge; se l'estensione c'è ma non corrisponde al tipo MIME allora può aggiungere l'estensione prevista oppure sostituirla a quella esistente (io propendo per il secondo meccanismo). Seguono esempi "patologici" nei quali il browser deve gestire la situazione:
Fattura 4:06 del 4\2\2006.pdf
"
(nome lecito su file system Unix ma non su Windows) il browser potrebbe
proporre all'utente un nome alternativo come "Fattura 4-06 del
4_2_2006.pdf
".
panorama.jpg
"
il cui tipo MIME viene riportato essere "text/plain
",
allora il browser potrebbe aprire il file con Notepad e potrebbe
proporre all'utente di salvarlo come "panorama_jpg.txt
"
per adeguarsi alle convenzioni del file system sottostante che mappano
il tipo MIME nell'estensione.
virus.exe
" e il tipo MIME indicato dal
server è un innocuo "image/jpeg
", il client dovrebbe
aprirlo come immagine come indicato dal suo tipo MIME, e non cercare
di eseguirlo; se l'utente vuole salvare il file, il browser dovrebbe
proporre una estensione appropriata alle convenzioni del sistema client,
ad esempio "virus.exe.jpeg
" oppure "virus.jpg
".
/
è vietato, mentre in Windows
non si possono usare :\/*?"<>|
. Il browser dovrebbe
applicare le restrizioni imposte dal file system sottostante; se tali
caratteri riservati compaiono nel nome del file, allora il browser
dovrebbe sostituirli opportunamente e proporre all'utente un nome di
file valido.
Fatta questa premessa su come le cose dovrebbero andare, vediamo nella pratica come si comportano i vari browser. La situazione è piuttosto variegata e i browser tendono ad alterare arbitrariamente il nome del file ed espongono l'utente a rischi di sicurezza con la gestione delle estensioni. Sebbene non sia compito del server rispondere delle mancanze che sono invece lato client, esponiamo anche i possibili rimedi che il programmatore può adottare per facilitare la vita all'utente. Alcuni rimedi richiedono di estrapolare non solo il browser usato, ma anche il sistema operativo.
Attenzione! I rimedi che propongo cercano di ovviare ai difetti che ho fin qui riscontrato. E' possibile che la mia analisi sia incompleta. E' possibile che taluni caratteri o combinazioni di caratteri che qui non ho considerato possano causare problemi inaspettati. In definitiva, lato server possiamo tentare il possibile, ma la responsabilità finale sul come vengono gestiti i caratteri, le estensioni e la sicurezza in generale, rimangono a carico del browser e dell'utente.
Chrome 31.
Rimpiazza i caratteri : / \ ? * " < > |
con -
(meno).
Ignora il tipo MIME proposto e si basa unicamente sulla estensione,
sicché se il tipo MIME è application/pdf
e
l'estensione è .txt
, il file finale mantiene l'estensione
sbagliata e viene poi aperto come testo.
Rimedio. Se l'estensione manca o non corrisponde al tipo MIME,
aggiungere l'estensione convenzionale (non tentare di sostituirla
all'apparente estensione esistente, che potrebbe non essere affatto una
estensione). Esempio: Panorama 11.dic
diventa
Panorama 11.dic.jpeg
.
MSIE 9.
Rimpiazza i caratteri : / \ ? * " < > |
con _
(underscore). Ignora il tipo MIME proposto e si basa unicamente sulla
estensione, sicché se il tipo MIME è application/pdf
e l'estensione è .txt
, il file finale mantiene l'estensione
sbagliata e viene poi aperto come testo.
Rimedio. Se l'estensione manca o non corrisponde al tipo MIME,
aggiungere l'estensione convenzionale (non tentare di sostituirla
all'apparente estensione esistente, che potrebbe non essere affatto una
estensione). Esempio: Panorama 11.dic
diventa
Panorama 11.dic.jpeg
.
MSIE 6 e 8.
Non tollera i caratteri :\/*?"<>|
Se
anche uno solo di questi caratteri compare nel nome, tipicamente
il browser sostituisce tutto il nome del file con un nome
generato casualmente, ad esempio un criptico "CAEFGR34..pdf
".
Si salvano lo slash e il back-slash: se uno di questi appare, esso viene
considerato separatore di pathfile, e quindi mantiene solo quello
che sta a destra di questo carattere. In definitiva "Fattura
3/2006.pdf
diventa "2006.pdf
". Il tipo MIME
non viene mai presentato all'utente, nè viene descritto in
alcun modo il contenuto del file. Se aggiungiamo il fatto che tra
le preferenze default del sistema operativo c'è quella di
nascondere le estensioni dalla vista degli utenti non smaliziati, ecco
che il nostro famigerato file virus.exe
spacciato per un
innocuo application/pdf
viene salvato tranquillamente ed
eseguito senza che l'utente possa conoscere né il tipo MIME,
nè la reale estensione. Rimedi. Sostituire i caratteri
vietati con underscore. Se l'estensione manca o non corrisponde al
tipo MIME, aggiungere l'estensione convenzionale (non tentare
di sostituirla all'apparente estensione esistente, che potrebbe non
essere affatto una estensione). Ad esempio, l'immagine JPEG di nome
Panorama:12/11/1980 12.00
deve diventare qualcosa come
Panorama_12_11_1980 12.00.jpg
Mozilla 1.7.15/Linux.
Sostituisce l'unico carattere riservato dal file system "/
"
con "-
", mentre lascia intatti tutti gli altri caratteri.
L'estensione del nome file, se presente, viene lasciata immutata
anche se non corrisponde alla convenzione genericamente adottata
sul sistema operativo sottostante. Però la dialog box
per il salvataggio del file si basa solo sulla estensione per
descrivere il tipo del file, anche se questa contrasta con
il tipo MIME indicato. L'estensione rimane comunque sempre ben
visibile. Rimedi. Se l'estensione manca o non corrisponde al
tipo MIME, aggiungere l'estensione prevista. Ad esempio se
una fotografia JPEG si chiama Panorama 12/11/1980
correggere in Panorama 12/11/1980.jpg
Opera 9.01/Linux.
Considera i caratteri "/\:
" come separatori di path,
quindi elimina tutto quello che precede questo carattere;
il nome del file risulta così troncato. La parentesi graffa
aperta "{
" ha un effetto strano: tutto quello che va da
questo carattere in poi viene cancellato. Il carattere ?
invece ha l'effetto opposto: da qui in poi la stringa viene cancellata.
Perché Opera presenti tutti questi strani comportamenti su
file system Unix-like non me lo spiego.
Quando si salva il file,
Opera controlla se l'estensione corrisponde al tipo MIME. Opera considera
"estensione" tutto quello che segue l'ultimo punto fermo nel nome del
file; pertanto se il nome del file include dei punti fermi (a parte quello
della eventuale estensione) il nome del file potrebbe risultare troncato
in modo imprevisto. Se il nome del file termina con una estensione che
non corrisponde al tipo MIME, allora Opera elimina l'estensione ritenuta
erronea e la sostituisce con quella prevista. Se il nome del file non
contiene una estensione, Opera aggiunge quella prevista. Rimedi.
Lato server, sostituire i caratteri riservati con underscore
o altro carattere innocuo. Se il nome del file non ha estensione oppure
ne ha una non adatta aggiungere l'estensione convenzionale.
Secondo queste regole il nome Panorama 12/11/1980
diventa Panorama 12_11_1980.jpg
Opera 9.01/Windows. Non ho fatto prove esaustive, ma probabilmente va applicato il filtro sui caratteri riservati del file system Windows, e l'estensione prevista va aggiunta se mancante o non adatta.
Browser/S.O. | Caratteri vietati | Carattere sostitutivo |
Assicura corrispondenza estensione/tipo MIME |
---|---|---|---|
Chrome 31.0/Windows Vista | : / \ ? * " < > | |
- |
No |
MSIE 9.0/Windows Vista | : / \ ? * " < > | |
_ |
No |
MSIE 8.0/Windows Vista | : / \ ? * " < > | |
_ |
No |
MSIE 6.0/Windows 98 | : / \ ? * " < > | |
No | |
Firefox 25.0/Windows Vista | : / \ ? * " < > | |
_ |
Sì |
Firefox 3.6/Windows Vista | : / \ ? * " < > | |
_ |
Sì |
Firefox 3.6/Linux | / |
_ |
No |
Mozilla 1.7.13/Linux | / |
- |
No |
Opera 9.01/Linux | : / \ { ? |
Sì | |
Opera 9.01/Windows 98 | N.D. | N.D. | Sì |
Risultati dei test su caratteri ed estensioni.
La tabella qui sopra riassume il comportamento dei vari browser provati rispetto ai caratteri presenti nel nome del file e rispetto alla corrispondenza tra estensione e tipo MIME. Mozilla si comporta bene per i caratteri: solo lo slash, unico carattere riservato in Linux, viene riconosciuto e sostituito con un meno. Opera su Windows e Firefox 3.6 su Windows e Linux si dimostrano molto rigorosi nella gestione delle estensioni e nella visualizzazione del tipo MIME corretto, salvando l'utente da brutte sorprese.
Tra i vari browser che ho provato non c'è una soluzione comune valida per tutti e capace di preservare il nome del file UTF-8. Non rimangono che tre alternative:
Usare l'ASCII. Semplice da implementare, valido per tutti i browser, ma supporta solo i nomi file composti da caratteri ASCII. Tutti i codici non-ASCII stampabili vanno eliminati oppure sostituiti con un segnaposto. Le estensioni dei nomi di file non vanno toccate, ma ricordare di fornire sempre il giusto tipo MIME. Quindi, niente vocali accentate, niente alfabeti dei paesi dell'est. Anche turchi, coreani, giapponesi e cinesi incontreranno qualche problema.
Offrire un comportamento differenziato in base al browser.
Basta guardare la variabile $_SERVER['HTTP_USER_AGENT']
: se
contiene MSIE oppure Gecko oppure Opera abbiamo identificato il browser.
Per MSIE potremo usare URL-Encode, mentre per gli altri due potremo usare la
codifica enhanced. Inoltre MSIE dà la precedenza alle estensioni
rispetto al MIME type: per proteggere i nostri utenti, nel caso potremmo
forzare l'estensione corretta al file prima di inviarlo al browser.
Configurazione dell'utente. L'automatismo di prima dovrebbe soddisfare la maggior parte degli utenti. Gli utenti più smaliziati potrebbero gradire una interfaccia di configurazione del download per stabilire quale codifica usare tra quelle che abbiamo proposto qui. Tra i parametri di configurazione possiamo includere anche il filtro dei caratteri speciali, e magari possiamo lasciare all'utente stesso la possibilità di definire questi caratteri speciali. Naturalmente l'interfaccia deve permettere di provare le impostazioni correnti, in modo che si possa procedere per tentativi. Il vantaggio di questa soluzione è che la nostra applicazione WEB risulta già pronta quando i vari browser si saranno messi daccordo. Il programma di test che propongo alla fine di questo articolo mostra un esempio di interfaccia che offre all'utente la possibilità di configurare le preferenze per il download e fare un test. In attesa che i produttori di browser trovino un accordo, questa è la soluzione più completa e rigorosa.
Spesso il download dei file dipende delle autorizzazioni dell'utente, e quindi viene coinvolto il login dell'utente e la sessione dell'utente per stabilire se egli è autorizzato a scaricare quel file. Inoltre, spesso le informazioni della sessione contengono la chiave primaria del DB, il path del file sul server o altre informazioni che non è bene inviare al client e che permettono al nostro programma di individuare il file che l'utente è autorizzato a scaricare. Vediamo allora quali interazioni ci sono tra le sessioni del PHP e il nostro il download dei file.
Il comportamento default del PHP quando si genera una sessione con
session_start()
è di inviare un insieme di headers HTTP che
impediscono il caching e lo storing dei files. Questo interferisce con il
salvataggio dei file scaricati, che potrebbe essere del tutto vietato dal
browser. Ad esempio, usando la configurazione PHP default, questo programma
produce questa risposta:<?php session_start(); echo "ciao"; ?>
HTTP/1.1 200 OK Date: Thu, 26 Oct 2006 15:04:49 GMT Server: Apache Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache X-Powered-By: PHP/5.1.1 Set-Cookie: PHPSESSID=628fd5de0dba53c92f66e6c8c1e14627; path=/ Transfer-Encoding: chunked Content-Type: text/html 4 ciao 0
La parte evidenziata in grassetto è il problema. No-cache significa che il file verrà scaricato ogni volta che l'utente richiede l'URL. No-store significa che il browser deve evitare di salvare il file scaricato nell'HD dell'utente per mantenere la cache, ma il browser potrebbe interpretare in modo restrittivo questa direttiva vietando del tutto il salvataggio dei dati. Per eliminare queste righe ci sono due modi:
session.cache_limiter = noneQuesto implica che tutti i nostri script devono gestire il caching in modo opportuno. Se non si gestisce il caching, c'è il rischio che il browser non esegua l'aggiornamento delle pagine generate dinamicamente, e applichi la logica "stesso URL, stessi dati". Una soluzione è mettere
header("Cache-Control: no-cache");
all'inizio di ogni script.
session_cache_limiter()
prima di session_start()
:
session_cache_limiter("none"); session_start();In questo secondo caso la funzione
session_start()
non genera
alcun header di controllo del caching. Il caching viene lasciato a discrezione
del browser, per cui eventuali ripetizioni del dowload da parte dell'utente
possono o non possono causare un effettivo altro trasferimento del file via
rete. A seconda della logica del programma dovremo eventualmente gestire noi
il caching corretto aggiungendo gli opportuni header HTTP. Il prossimo
paragrafo dà alcuni suggerimenti.
Gli utenti ripetono spesso il download di uno stesso file: vogliono rivedere un certo documento; hanno dimenticato dove hanno salvato il download precedente; hanno dimenticato di averlo già scaricato; hanno interrotto distrattamente il download e vogliono riprendere il download interrotto; si divertono a cliccare ripetutamente sul link; varie ed eventuali. Il caching secondo l'HTTP (RFC 2616) è un tema piuttosto complicato. La cache viene implementata dal browser e dal proxy server (se presente). Il server può specificare con quali modalità le informazioni mantenute nella cache devono essere gestite. Il supporto della cache lato server aiuta a risparmiare banda e rende più veloce la navigazione. Quando sono coinvolti file particolarmente grandi, la gestione corretta della cache è indispensabile. Diamo qualche suggerimento al riguardo, rimandando all'RFC 2616 per i dettagli.
Nessun provvedimento. E' quello che abbiamo fatto finora. Il download viene richiesto dall'utente cliccando su di un'ancora. Il browser o il proxy, in base a una qualche impostazione di preferenza, decide se è ora di ripetere la richiesta dello stesso URL oppure ripresentare all'utente il file già disponibile nella cache. Questo meccanismo può creare problemi se lo stesso URL (al quale corrisponde il nostro programma di download) viene riutilizzato più volte per scaricare file diversi; spesso il file specifico da scaricare di volta in volta viene deciso dal programma con una sua logica interna, ma dal punto di vista del browser l'URL dal quale proviene il file è unico, per cui è anche unica l'entrata della cache associata ad esso. Trucco: mettere un parametro nell'URL che sia associato univocamente al file specifico, per esempio il suo nome, la sua PK nel data base, il suo digest di controllo o il suo path assoluto sul server. Per esempio:
http://www.miosito.it/download.php?file=pippo.pdf
Notare che il parametro file=pippo.pdf
deve essere
adeguatamente validato dal programma, altrimenti consentiremmo di
scaricare file arbitrari fuori dal controllo del nostro programma.
Vietare il caching.
Basta aggiungere l'header header("Cache-Control: no-cache");
Questo costringe il browser a ripetere sempre la richiesta GET del file
per intero, disabilitando ogni meccanismo di caching.
Gestire richieste condizionali.
Il browser (o il proxy) può ripetere la richiesta GET di
un file subordinata al fatto che il file in questione sia stato
aggiornato rispetto alla data salvata nella cache. In pratica il
browser chiede: mandami il file solo se aggiornato rispetto alla
volta precedente.
La richiesta condizionale si riconosce per via della presenza dell'header
$_SERVER['HTTP_IF_MODIFIED_SINCE']
contenente la data di
riferimento del browser.
Il server risponde con il codice di stato 304
(Not Modified) oppure con il codice 200 e il file a seguire.
Gestire richieste parziali di file.
A volte il browser richiede solo la parte iniziale di un file, giusto
per presentare un'anteprima all'utente, e poi prosegue nello scaricare
il resto con richieste successive. Lo stesso meccanismo è utile
se il trasferimento è stato interrotto per un qualche motivo
e il browser vuole salvare la parte già disponibile. Questo
tipo di richiesta si riconosce per via della presenza dell'header
$_SERVER['HTTP_RANGE']
che contiene il range (o i range)
che interessano. Il server risponderà con un 206 alla richiesta
parziale, oppure con un codice di errore se la richiesta non è
valida.
Per verificare il comportamento dei browser alle varie codifiche e il comportamente nella gestione di tipo MIME ed estensione, ho scritto un piccolo programma di test che si trova qui:
php-file-download-test.cgi
Questo programma è utile sia per eseguire le prove sulla propria combinazione browser/sistema operativo, sia come possibile modello di implementazione dell'interfaccia utente, sia come sorgente pronto all'uso.
Il programma offre in fondo alla pagina l'ancora per scaricare il sorgente di sè stesso. Per eseguire i test procede nel modo seguente.
Un nome di file viene fornito in una casella di input a linea singola e memorizzato dal programma come stringa UTF-8.
Poi bisogna scegliere quale charset usare per inviare il nome del file al client. Ci sono tre possibilità l'UTF-8 è ovviamente quella preferibile; l'ISO-8859-1 dovrebbe essere intepretato per default dai browser più antiquati; l'ASCII è l'ultima risorsa. All'atto del download il nome del file viene convertito nel charset scelto prima di essere inviato al browser. Per i charset diversi da UTF-8 ovviamente c'è la possibilità che qualche carattere non sia convertibile: i caratteri non convertibili vengono rappresentati con l'underscore.
Alcuni browser fanno i capricci se il nome del file contiene certi caratteri. L'opzione di filtro, quando attiva, li converte in underscore.
Infine possiamo scegliere la codifica da usare, come abbiamo descritto in questo articolo.
Infine, il bottone Try Download rimanda alla pagina del download. La pagina del download mostra le linee di header che verranno usate, l'ancora per scaricare effettivamente il file e l'ancora per ritornare alla maschera di configurazione del programma. Il file scaricato è un PDF di 5 KB.
Le cose da provare con ogni combinazione di browser/sistema sono:
Umberto Salsi | Commenti | Contatto | Mappa | Home / Indice sezione |
Still no comments to this page. Use the Comments link above to add your contribute.