Comment forcer le téléchargement d’un fichier en PHP ?

Forcer le téléchargement d’un fichier consiste à forcer le navigateur à enregistrer le fichier sur votre machine au lieu de l’afficher directement dans une fenêtre (image, PDF, etc.).

En outre, ce fonctionnement permet de restreindre, si nécessaire, le téléchargement des fichiers à certains utilisateurs, suivant leur niveau d’accès par exemple.

Je vais donc vous expliquer comment gérer le téléchargement de fichier via PHP.

Le sujet

Nous souhaitons permettre l’accès à certains fichiers seulement si l’utilisateur est enregistré et connecté au site.

La gestion de l’inscription et de la connexion ne sera pas vu ici, car ce n’est pas le sujet. Nous considérons alors la personne connectée au site si la variable $_SESSION[« id »] existe et est renseignée.

Les dossiers et fichiers

Pour restreindre l’accès au fichier, il faut que ces derniers soient stockés dans un répertoire inaccessible par le serveur web. Il doit être impossible d’accéder aux fichiers en tapant leur nom dans la barre d’adresse.

Je vous propose deux solutions, une avec un fichier htaccess, le second en jouant avec le DocumentRoot.

Pour la solution avec le fichier htaccess, voici l’arbre des fichiers et dossiers utilisé :

files
  `- manuel-php.pdf
  `- .htaccess
download.php
index.php

Par défaut, le fichier manuel-php.pdf est accessible directement via l’adresse (ex: http://localhost/files/manuel-php.pdf), ce qui n’est pas bon du tout. Il faut donc ajouter dans le fichier .htaccess présent dans files l’instruction suivante :

Deny from all

Si vous tentez d’accéder au fichier, vous aurez un message de type 403 (accès interdit).

La seconde méthode (celle que je préconise), consiste à sortir le dossier files du DocumentRoot (la racine du serveur web). Exemple :

files
  `- manuel-php.pdf
www
  `- download.php
  `- index.php

Le DocumentRoot doit pointer sur www. Si vous accédez à l’adresse racine (ex : http://localhost), vous devez arriver sur le fichier index.php. Ainsi, le fichier htaccess devient inutile et vos fichiers sont protégés d’un accès directe.

Le fichier download.php

Ce fichier va être utilisé afin de fournir les fichiers à télécharger à l’utilisateur. Nous allons construire le fichier ligne par ligne. Le code source entier sera disponible en fin d’article.

Vous pouvez aller directement au code source si vous ne souhaitez pas lire tout l’article.

Tout d’abord, nous devons modifier un paramètre lié à la configuration de PHP :

<?php
set_time_limit(0);

Par défaut, PHP interrompt le script lorsque le temps d’exécution atteint cette limite. Nous risquons alors d’envoyer des fichiers tronqués. Il faut définir cette valeur à 0 afin de désactiver ce comportement.

Nous vérifions maintenant que l’utilisateur ait accès au téléchargement des fichiers :

session_start();

if (!isset($_SESSION["user_id"])) {
    header("HTTP/1.1 403 Forbidden");
    exit;
}

J’utilise ici la fonction header permettant de définir les entêtes HTTP. J’utilise le code 403 puisqu’il sert à spécifier que l’accès sur la ressource n’est pas autorisé.

Cette partie dépend entièrement de votre application. En effet, selon votre application, vous allez gérer la gestion d’accès différemment. À vous d’adapter le code. Vous pouvez supprimer cette partie si vous souhaitez simplement faire un test.

Ensuite, il faut spécifier quel fichier doit être téléchargé, nous passons pour cela par un simple paramètre d’URL. Je vais utiliser « file » :

if (empty($_GET["file"])) {
    header("HTTP/1.1 404 Not Found");
    exit;
}
if(basename($_GET["file"]) != $_GET["file"]) {
    header("HTTP/1.1 400 Bad Request");
    exit;
}
$name = $_GET["file"];

$filename = dirname(__FILE__)."/../files/".$name;
if (!is_file($filename) || !is_readable($filename)) {
    header("HTTP/1.1 404 Not Found");
    exit;
}
$size = filesize($filename);

Pour télécharger le fichier manuel-php.pdf, il suffit de faire appel à http://localhost/download.php?file=manuel-php.pdf

Après avoir testé que la variable $_GET[« file »] existe et ne soit pas vide, on fait appel à basename afin d’être sur que ce ne soit qu’un nom de fichier. En effet si on indique une adresse du genre http://localhost/download.php?file=../www/download.php on enclencherait le téléchargement de download.php. On imagine ce qui se passerait avec un fichier de configuration contenant les identifiants d’une base de données.

On en profite pour récupérer la taille du fichier à l’aide de la fonction filesize.

$filename contient le chemin complet vers le fichier à télécharger.

Afin d’éviter une erreur sur certains navigateurs, on ajoute ces lignes :

if (ini_get("zlib.output_compression")) {
    ini_set("zlib.output_compression", "Off");
}

Ce code désactive la compression automatique des données dans le cas où elle aurait été activée.

Ajoutez maintenant cette ligne :

session_write_close();

session_write_close permet de fermer la session en cours. Si la session reste ouverte avant l’envoi des données, vous ne pourrez plus naviguer sur le site tant que le téléchargement ne sera pas terminé. J’ai mis du temps à trouver comment résoudre ce problème. Bien entendu, si vous utilisez un framework, pensez à adapter votre code.

Maintenant, nous désactivons toutes mise en cache.

header("Cache-Control: no-cache, must-revalidate");
header("Cache-Control: post-check=0,pre-check=0");
header("Cache-Control: max-age=0");
header("Pragma: no-cache");
header("Expires: 0");

Ces cinqs lignes permettent de couvrir tout les navigateurs, normalement.

Ajoutez maintenant :

header("Content-Type: application/force-download");
header('Content-Disposition: attachment; filename="'.$name.'"');

Ces deux lignes force le navigateur à télécharger le fichier. On n’oublie pas de spécifier un nom de fichier qui sera utilisé pour pré-remplir le champ. C’est très pratique pour l’utilisateur.

Ensuite, on spécifie la taille du fichier à télécharger :

header("Content-Length: ".$size);

C’est un point très important. Par défaut, le navigateur ne saura pas combien d’octets il devra télécharger. Dans la fenêtre de téléchargement, il y a en général le temps restant avant la fin du téléchargement, mais aussi la progression (ex: 1.3Mo / 40Mo). Or, sans connaître la taille des données, il ne saura pas afficher ces informations. Il sera impossible pour l’utilisateur de savoir où en est le téléchargement.

En spécifiant l’entête Content-Length, vous indiquez la taille totale des données à télécharger. Les entêtes étant téléchargées en premier, il saura en avance combien d’octets devront être récupérés. Il pourra alors l’afficher dans sa boite de téléchargement. C’est un réel confort pour l’utilisateur.

Pour terminer, on envoi le contenu du fichier :

readfile($filename);

Code source complet

<?php
// désactive le temps max d'exécution
set_time_limit(0);

// démarrage de la session
session_start();

// vérifie que l'utilisateur est connecté
if (!isset($_SESSION["user_id"])) {
    header("HTTP/1.1 403 Forbidden");
    exit;
}

// on a bien une demande de téléchargement de fichier
if (empty($_GET["file"])) {
    header("HTTP/1.1 404 Not Found");
    exit;
}
// le nom doit être un nom de fichier
if(basename($_GET["file"]) != $_GET["file"]) {
    header("HTTP/1.1 400 Bad Request");
    exit;
}
$name = $_GET["file"];

// vérifie l'existence et l'accès en lecture au fichier
$filename = dirname(__FILE__)."/../files/".$name;
if (!is_file($filename) || !is_readable($filename)) {
    header("HTTP/1.1 404 Not Found");
    exit;
}
$size = filesize($filename);

// désactivation compression GZip
if (ini_get("zlib.output_compression")) {
    ini_set("zlib.output_compression", "Off");
}

// fermeture de la session
session_write_close();

// désactive la mise en cache
header("Cache-Control: no-cache, must-revalidate");
header("Cache-Control: post-check=0,pre-check=0");
header("Cache-Control: max-age=0");
header("Pragma: no-cache");
header("Expires: 0");

// force le téléchargement du fichier avec un beau nom
header("Content-Type: application/force-download");
header('Content-Disposition: attachment; filename="'.$name.'"');

// indique la taille du fichier à télécharger
header("Content-Length: ".$size);

// envoi le contenu du fichier
readfile($filename);

Conclusion

Vous voila avec un bon début pour mettre en place un système de téléchargement de fichier. Toutefois, ce code ne permet pas de gérer la mise en pause et la reprise du téléchargement. Cette action provoque une erreur.

Mais ne vous inquiétez pas, c’est le sujet du prochain article 😉

20 réflexions sur « Comment forcer le téléchargement d’un fichier en PHP ? »

  1. Merci.

    Je viens de mettre à jour l’article car j’ai oublié une information importante, elle concerne la définition du temps max d’exécution du script (set_time_limit).

    Pensez à mettre à jour.

  2. Je rapporte une erreur fréquente: [headers already sent]
    Si un caractère à déjà été envoyé au navigateur avant les clauses header() qui configurent le téléchargement, alors le fichier sera affiché dans le navigateur à la suite de ce caractère, et non téléchargé. Avec ce caractère, on est déjà en « mode affichage » et on ne peut plus modifié en télélchargement.

    Visualiser le code source de la page via le navigateur peut aider à trouver ce carcatère.

  3. Bonjour,
    Excellent tutoriel qui m’a fait gagner un temps fou.
    J’ai néanmoins constaté un problème que je n’arrive pas à régler depuis hier.

    Si j’utilise :
    $fullName = « ./documentation/doc1.pdf »;
    les / sont remplacés par des _ et le système cherche à télécharger le fichier ._documentation_doc1.pdf. Aucune erreur n’est interceptée, mais le document téléchargé est vide et porte le nom ._documentation_doc1.pdf

    En revanche, si j’utilise :
    $fullname = dirname(__FILE__). »/documentation/doc1.pdf »;
    Tout se passe normalement.

    J’ai l’impression que c’est la fonction readfile() qui me joue ce tour. Qu’en pensez-vous ?

    • Désolé, mon problème ne venait pas de là, mais à force de tourner, je me suis pris les pieds dans le tapis.

      Le problème venait de la ligne d’instruction :
      header(‘Content-Disposition: attachment; filename= »‘.$fichier.' »‘);

      $fichier ne doit contenir que le nom du fichier, donc sans le path. Si on met le path et le nom du fichier, on obtient l’erreur qui m’a fait tourner en bourrique.

      Merci pour la rapidité de votre réponse.

      • C’est bien ce que je pensais mais je préférais voir le code avant.
        Sur les systèmes de fichier, il n’est pas possible de créer des fichiers contenant des slashes. Ces derniers font partis des caractères interdits puisqu’ils servent de séparateur dans les chemins de répertoire.

        Problème résolu, c’est le principal.

  4. Voici mon code :
    $size = filesize($fichierHeberge);
    if (ini_get(« zlib.output_compression »))
    {
    ini_set(« zlib.output_compression », « Off »);
    }
    header(« Cache-Control: no-cache, must-revalidate »);
    header(« Cache-Control: post-check=0,pre-check=0 »);
    header(« Cache-Control: max-age=0 »);
    header(« Pragma: no-cache »);
    header(« Expires: 0 »);
    header(« Content-Type: application/force-download »);
    header(‘Content-Disposition: attachment; filename= »‘.basename($fichierHeberge).' »‘);
    header(« Content-Length: « .$size);
    readfile($fichierHeberge);

    Tout se passe bien, la boîte de dialogue s’affiche, je peux enregistrer le document pdf… mais lorsque j’essaie d’ouvrir ce document, je reçois un message d’erreur de Acrobat Reader qui dit que le type de fichier n’est pas pris en charge ou que le fichier est endommagé.

    Pourtant, il s’ouvre bien avant d’être envoyé.

    Avez-vous une idée d’où vient l’erreur ?

    Merci

  5. Merci pour ce tutoriel car cela m’a permis de connaitre comment faire le telechargement de fichier. Le petit probleme qui me fatigue c’est de savoir comment faire l’inscription ou la connexion car vous n’avez pas montrer un petit tuto la sur

  6. Bonjour,

    Le code fonctionne bien avec mes fichiers pdf, mais pas avec les jpg.
    Le fichier jpg est bien telechargé, mais lorsque je l’ouvre il est corrompu.
    « Impossible d’ouvrir le fichier, Il est peut-être endommagé ou d’un format non reconnu par Aperçu. »

    par contre , un lien direct vers mon fichier jpg fonctionne.

    Une idée ? merci par avance

  7. Bonsoir merci pour ce tuto clair, avant de le tester j’aimerai savoir si c’est faisable avec un fichier mp3 et une sécurisation via token ?

    Merci.

  8. Bonjour,
    Merci pour ce super tuto. J’ai un petit souci néanmoins dans l’application de celui ci. En effet, il me propose bien le téléchargement du fichier, mais… il le modifie pour mettre le code de ma page AUTOUR du contenu de mon fichier (par exemple sur un fichier text avec « test » dedans, il m’a afficher tout mon header/body et a insérer test dans le body avant de mettre le reste de mon code.

    Un indice sur le pourquoi du comment ? (accessoirement, quand je place en commentaire la partie désactivation de la mise en cache, il me pose le contenu de mon fichier donc « test » en plein milieu de ma page web)

    Merci de votre aide !

  9. Bonjour,

    Tout d’abord merci pour votre tutoriel !
    J’aimerais savoir s’il est possible de lancer automatiquement le téléchargement d’un fichier X sans interaction avec l’utilisateur ?

    Merci d’avance !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.