Tutoriel
Partie 15 — Upload de fichiers : images, contrôles, stockage sécurisé

Partie 15 — Upload de fichiers : images, contrôles, stockage sécurisé

Guide complet upload fichiers en PHP : formulaire, $_FILES, contrôle taille/MIME, stockage sécurisé, renommage, anti-virus, images (compression, resize), erreurs fréquentes.

PHP 52 Mis à jour 11 hours ago
Conseil : lisez d’abord les sections clés, puis essayez un QCM lié à la même notion pour valider votre compréhension.

Partie 15 — Upload de fichiers : images, contrôles, stockage sécurisé

L’upload de fichiers est un des modules les plus sensibles d’un site. Beaucoup de débutants font “un upload qui marche”, mais oublient que le fichier vient de l’utilisateur. Et si tu acceptes n’importe quoi, tu peux ouvrir une porte énorme : fichiers exécutables, scripts cachés, surcharge disque, images énormes, attaques par double extension…

Dans cette partie, on va apprendre à faire un upload comme en production : validations, sécurité, organisation, et traitement des images. L’objectif : que tu puisses intégrer ça dans un vrai projet (profil utilisateur, images d’articles, pièces jointes…).

👉 On va couvrir : formulaire + $_FILES + erreurs + contrôles (taille/MIME) + stockage sécurisé + images (resize/compress).


1) Le formulaire HTML (obligatoire) : enctype="multipart/form-data"

Pour envoyer un fichier, ton formulaire doit impérativement contenir : enctype="multipart/form-data". Sans ça, ton fichier n’arrive pas côté serveur.

<form action="upload.php" method="POST" enctype="multipart/form-data">
  <label>Choisir une image :</label>
  <input type="file" name="avatar" accept="image/*" required>

  <button type="submit">Uploader</button>
</form>
  

Le accept="image/*" aide le navigateur, mais attention : ce n’est pas une sécurité. Un attaquant peut envoyer ce qu’il veut. La vraie validation se fait côté PHP.


2) Comprendre $_FILES : ce que PHP reçoit réellement

Quand tu envoies un fichier, PHP remplit le tableau $_FILES. Pour un input nommé avatar, tu obtiens :

  • $_FILES['avatar']['name'] : nom original (pas fiable)
  • $_FILES['avatar']['type'] : type annoncé (pas fiable)
  • $_FILES['avatar']['tmp_name'] : chemin temporaire sur le serveur
  • $_FILES['avatar']['error'] : code erreur upload
  • $_FILES['avatar']['size'] : taille en octets
<?php
// upload.php
echo "<pre>";
print_r($_FILES);
echo "</pre>";
?>
  

⚠️ IMPORTANT : name et type viennent du client. Ne fais jamais confiance.


3) Gérer les erreurs d’upload (UPLOAD_ERR_*)

Avant toute chose, on vérifie $_FILES['avatar']['error']. Sinon tu risques de traiter un upload incomplet ou inexistant.

<?php
$file = $_FILES["avatar"] ?? null;

if (!$file) {
    die("Aucun fichier reçu.");
}

if ($file["error"] !== UPLOAD_ERR_OK) {
    // codes courants :
    // UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE...
    die("Erreur upload : " . $file["error"]);
}
?>
  

En production, tu traduis ces codes en messages clairs pour l’utilisateur (ex: “Fichier trop grand”, “Aucun fichier sélectionné”…).


4) Contrôle de la taille : éviter les fichiers énormes

Exemple : tu acceptes maximum 2 Mo (2 * 1024 * 1024).

<?php
$maxSize = 2 * 1024 * 1024; // 2MB

if ($file["size"] > $maxSize) {
    die("Fichier trop volumineux (max 2 Mo).");
}
?>
  

Pense aussi à configurer ton serveur/PHP : upload_max_filesize et post_max_size dans php.ini, sinon l’upload sera bloqué avant même d’arriver dans ton script.


5) Extension ≠ sécurité : contrôler le vrai type (MIME)

Beaucoup de devs valident juste l’extension : .jpg, .png. Mais un attaquant peut envoyer un fichier dangereux renommé en “image.jpg”. Ce qui compte vraiment : le contenu.

5.1 Whitelist d’extensions (premier filtre)

<?php
$allowedExt = ["jpg", "jpeg", "png", "webp"];

$originalName = $file["name"];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

if (!in_array($ext, $allowedExt, true)) {
    die("Extension non autorisée.");
}
?>
  

5.2 Contrôle MIME (vrai filtre) avec finfo_file()

finfo inspecte le fichier sur le serveur et détecte son type réel.

<?php
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file["tmp_name"]);

$allowedMime = [
  "image/jpeg",
  "image/png",
  "image/webp",
];

if (!in_array($mime, $allowedMime, true)) {
    die("Type de fichier non autorisé : " . htmlspecialchars($mime));
}
?>
  

✅ Bonne pratique : extension + MIME + (pour images) vérification de dimensions.


6) Sécurité : ne jamais enregistrer le nom original tel quel

Le nom original peut contenir :

  • des espaces / caractères spéciaux
  • des chemins (../../) dans certains cas
  • des collisions (deux utilisateurs uploadent image.jpg)
  • des noms très longs, difficiles à gérer

Solution : générer un nom unique côté serveur (hash, uuid, timestamp).

<?php
$unique = bin2hex(random_bytes(16)); // 32 caractères
$newName = $unique . "." . $ext;
?>
  

Tu peux stocker le nom original en base si tu veux l’afficher, mais tu ne l’utilises pas comme nom de fichier réel sur disque.


7) Stockage sécurisé : où mettre les fichiers ?

La règle la plus importante :

👉 Ne mets jamais des fichiers uploadés dans un dossier où PHP peut être exécuté.

Deux stratégies classiques :

7.1 Dossier public, mais verrouillé

Exemple : /public/uploads. Tu ajoutes des règles serveur pour empêcher l’exécution de scripts (ex: .php).

⚠️ Selon ton hébergement (Apache/Nginx), la configuration change. L’idée : interdire l’exécution dans uploads.

7.2 Dossier hors public (recommandé)

Exemple : /storage/uploads (non accessible directement). Ensuite, tu crées un script PHP qui sert le fichier après autorisation (contrôle accès / permissions).

Cas réel : documents sensibles (CIN, CV, fichiers privés). Tu vérifies l’identité de l’utilisateur avant d’afficher/télécharger.


8) Enregistrer le fichier : move_uploaded_file()

PHP stocke temporairement le fichier dans tmp_name. Pour le garder, tu dois le déplacer avec move_uploaded_file().

<?php
$uploadDir = __DIR__ . "/uploads"; // dossier local
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

$destination = $uploadDir . "/" . $newName;

if (!move_uploaded_file($file["tmp_name"], $destination)) {
    die("Impossible d'enregistrer le fichier.");
}

echo "Upload réussi : " . htmlspecialchars($newName);
?>
  

Note : move_uploaded_file vérifie que le fichier vient bien d’un upload HTTP, donc c’est plus sûr qu’un simple rename().


9) Spécial images : vérifier dimensions + compresser + redimensionner

Une image peut être “valide” mais énorme (8000x8000). Résultat : page lente, stockage gaspillé, risque de crash mémoire. Donc on contrôle aussi les dimensions.

9.1 Vérifier dimensions avec getimagesize()

<?php
$info = getimagesize($file["tmp_name"]);
if ($info === false) {
    die("Ce fichier n'est pas une image valide.");
}

$width  = $info[0];
$height = $info[1];

if ($width > 3000 || $height > 3000) {
    die("Image trop grande (max 3000x3000).");
}
?>
  

9.2 Mini stratégie “simple” : limiter taille + garder qualité

Le traitement (resize/compress) dépend de ta stack (GD, Imagick, librairies). En PHP natif, GD est souvent disponible. Voici un exemple “pragmatique” : convertir en JPEG et réduire à 1200px max.

<?php
function resizeJpeg(string $src, string $dst, int $maxWidth = 1200, int $quality = 82): bool
{
    $info = getimagesize($src);
    if ($info === false) return false;

    [$w, $h] = $info;
    $ratio = $w / $h;

    if ($w <= $maxWidth) {
        // pas besoin de resize, on peut juste copier
        return copy($src, $dst);
    }

    $newW = $maxWidth;
    $newH = (int) round($newW / $ratio);

    $srcImg = imagecreatefromstring(file_get_contents($src));
    if (!$srcImg) return false;

    $dstImg = imagecreatetruecolor($newW, $newH);

    imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0, $newW, $newH, $w, $h);

    $ok = imagejpeg($dstImg, $dst, $quality);

    imagedestroy($srcImg);
    imagedestroy($dstImg);

    return $ok;
}
?>
  

✅ Cas réel : images d’articles, avatars, thumbnails. Tu normalises les images pour qu’elles soient rapides et homogènes.

⚠️ Attention mémoire : pour des images énormes, GD peut consommer beaucoup. Dans les gros projets, Imagick (si dispo) est souvent plus robuste.


10) Upload multiple : plusieurs fichiers en une fois

Pour permettre plusieurs fichiers : name="photos[]" + multiple.

<input type="file" name="photos[]" multiple>
  

Côté PHP, $_FILES['photos'] devient un tableau de tableaux. Tu boucles.

<?php
$photos = $_FILES["photos"] ?? null;
if (!$photos) die("Aucun fichier.");

$count = count($photos["name"]);

for ($i = 0; $i < $count; $i++) {
    if ($photos["error"][$i] !== UPLOAD_ERR_OK) continue;

    $tmp  = $photos["tmp_name"][$i];
    $name = $photos["name"][$i];
    $size = $photos["size"][$i];

    // Ici : validation + move_uploaded_file pour chaque fichier
    echo "Reçu : " . htmlspecialchars($name) . " (" . $size . " octets)<br>";
}
?>
  

11) Anti-abus : limiter le nombre, logs, quotas, nettoyage

En production, tu veux éviter :

  • un utilisateur qui upload 500 fichiers
  • un bot qui remplit ton disque
  • des fichiers inutilisés qui s’accumulent

Solutions classiques :

  • Limiter le nombre (ex: max 5 fichiers par upload)
  • Limiter par compte (quota : 100 Mo max par utilisateur)
  • Nettoyage (cron qui supprime les fichiers orphelins)
  • Logs (garder trace des uploads : IP, user_id, date)

✅ Conseil : associe chaque fichier à une ligne en base de données (id, user_id, path, mime, size, created_at). Comme ça, tu maîtrises tout.


12) Exemple complet (propre) : upload image avatar

Voici un script “réaliste” : validations + nom unique + move + message final. Tu peux l’adapter à ton projet.

<?php
date_default_timezone_set("Africa/Casablanca");

function fail(string $msg): void {
    die("<div style='font-family:Tahoma; padding:12px; background:#fff1f2; border-left:4px solid #fb7185;'>"
        . htmlspecialchars($msg)
        . "</div>");
}

$file = $_FILES["avatar"] ?? null;
if (!$file) fail("Aucun fichier reçu.");

if ($file["error"] !== UPLOAD_ERR_OK) fail("Erreur upload (code " . $file["error"] . ").");

$maxSize = 2 * 1024 * 1024;
if ($file["size"] > $maxSize) fail("Fichier trop volumineux (max 2 Mo).");

$ext = strtolower(pathinfo($file["name"], PATHINFO_EXTENSION));
$allowedExt = ["jpg","jpeg","png","webp"];
if (!in_array($ext, $allowedExt, true)) fail("Extension non autorisée.");

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file["tmp_name"]);
$allowedMime = ["image/jpeg","image/png","image/webp"];
if (!in_array($mime, $allowedMime, true)) fail("Type non autorisé : " . $mime);

$imgInfo = getimagesize($file["tmp_name"]);
if ($imgInfo === false) fail("Image invalide.");

[$w, $h] = $imgInfo;
if ($w > 3000 || $h > 3000) fail("Image trop grande (max 3000x3000).");

$uploadDir = __DIR__ . "/uploads/avatars";
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);

$unique = bin2hex(random_bytes(16));
$newName = $unique . "." . $ext;
$dest = $uploadDir . "/" . $newName;

if (!move_uploaded_file($file["tmp_name"], $dest)) fail("Impossible d'enregistrer l'image.");

echo "<div style='font-family:Tahoma; padding:12px; background:#ecfeff; border-left:4px solid #06b6d4;'>";
echo "✅ Upload réussi : <strong>" . htmlspecialchars($newName) . "</strong><br>";
echo "MIME : " . htmlspecialchars($mime) . " — Taille : " . (int)$file["size"] . " octets";
echo "</div>";
?>
  

Tu peux ensuite :

  • enregistrer $newName en base
  • créer une version “thumbnail”
  • supprimer l’ancienne image avatar quand l’utilisateur change

13) Erreurs fréquentes (à éviter absolument)

  • Oublier enctype="multipart/form-data"
  • Utiliser $_POST au lieu de $_FILES
  • Faire confiance à $_FILES['type']
  • Stocker le nom original tel quel
  • Enregistrer dans un dossier où PHP peut s’exécuter
  • Ne pas limiter la taille/dimensions
  • Ne pas gérer l’erreur UPLOAD_ERR_*

14) Mini-exercices (pour devenir à l’aise)

  1. Ajoute un contrôle “max 5 fichiers” sur un upload multiple.
  2. Stocke en base : user_id, path, mime, size, created_at.
  3. Crée une fonction qui supprime un fichier et sa ligne DB (upload “propre”).
  4. Crée une thumbnail 300x300 pour chaque image uploadée.

Conclusion

Un upload de fichiers, ce n’est pas juste “move_uploaded_file et c’est fini”. C’est un module de sécurité et de qualité. Avec les bonnes validations (taille + extension + MIME + dimensions) et un stockage organisé (nom unique, dossier sécurisé), tu peux uploader des images et documents sans prendre de risques inutiles.

Si tu veux, dans la Partie 16 on peut enchaîner sur : emails (SMTP), envoi d’images, ou génération PDF — des modules très demandés en vrai projet.