Prévisualisation d'image uploadée via iframe avec PHP : compatibilité multi-navigateurs

Le rôle de l'iframe dans l'upload de fichiers pour navigateurs anciens

Dans le contexte actuel de développement web, où les API modernes comme Fetch et FormData sont largement supportées, il est facile d'oublier que la fonctionnalité d'upload avec prévisualisation devait auterfois fonctionner sur des navigateurs tels qu'Internet Explorer 7. Cette section explore comment l'élément iframe a été utilisé pour simuler un comportement asynchrone, contournant les limitations des anciens navigateurs qui ne supportaient pas les requêtes XMLHttpRequest pour l'envoi de fichiers.

Configuration du formulaire et ciblage vers l'iframe

La technique repose sur l'attribut target d'un formulaire HTML. En définissant sa valeur sur le nom d'un iframe caché, la réponse du serveur est chargée dans cet iframe au lieu de provoquer un rechargement de la page principale. Cela crée une illusion d'envoi asynchrone.

<!-- Structure HTML de base -->
<div id="conteneur-upload">
  <input type="file" id="selecteur-fichier" accept="image/*">
  <button type="button" id="btn-envoi">Envoyer</button>
  <div id="statut"></div>
  <img id="apercu" src="" alt="Prévisualisation">
</div>
<!-- L'iframe cible sera généré dynamiquement par le script -->

Gestion JavaScript : création dynamique et communication

Le code côté client crée un iframe unique pour chaque opération d'upload, définit le formulaire pour qu'il cible cet iframe, et écoute l'événement load sur l'iframe pour extraire la réponse du serveur.

const etat = {
  iframeActif: null,
  timeoutId: null,
  dureeTimeout: 30000
};

function demarrerTeleversement() {
  const fichier = document.getElementById('selecteur-fichier').files[0];
  if (!fichier || !validerFichier(fichier)) return;

  // Générer un identifiant unique pour l'iframe
  const identifiant = 'cadre_' + Date.now();
  const cadre = document.createElement('iframe');
  cadre.name = identifiant;
  cadre.style.display = 'none';

  // Attacher un gestionnaire d'événement une fois le cadre chargé
  cadre.onload = function() {
    clearTimeout(etat.timeoutId);
    repondreDepuisIframe(cadre);
    nettoyerCadre(cadre);
  };

  cadre.onerror = function() {
    clearTimeout(etat.timeoutId);
    afficherMessage('Échec réseau ou timeout.', true);
    nettoyerCadre(cadre);
  };

  document.body.appendChild(cadre);
  etat.iframeActif = cadre;

  // Configurer et soumettre le formulaire
  const formulaire = creerFormulaireTemporaire(identifiant, fichier);
  formulaire.submit();

  etat.timeoutId = setTimeout(() => {
    afficherMessage('Délai d\'attente dépassé.', true);
    nettoyerCadre(cadre);
  }, etat.dureeTimeout);
}

function creerFormulaireTemporaire(nomCadre, fichier) {
  const form = document.createElement('form');
  form.method = 'POST';
  form.action = '/upload.php';
  form.enctype = 'multipart/form-data';
  form.target = nomCadre;
  form.style.display = 'none';

  // Cloner l'input fichier pour éviter de perturber l'interface
  const champFichier = document.getElementById('selecteur-fichier').cloneNode();
  champFichier.name = 'image';
  form.appendChild(champFichier);

  document.body.appendChild(form);
  return form;
}

function repondreDepuisIframe(cadre) {
  try {
    // Accéder au contenu de l'iframe (seulement si même origine)
    const doc = cadre.contentDocument || cadre.contentWindow.document;
    const textarea = doc.querySelector('textarea');
    
    if (textarea) {
      const donnees = JSON.parse(textarea.value);
      traiterReponse(donnees);
    } else {
      // Tentative de récupération du texte brut
      const texte = doc.body.innerText.trim();
      if (texte) {
        const donnees = JSON.parse(texte);
        traiterReponse(donnees);
      }
    }
  } catch (erreur) {
    console.error('Erreur de parsing:', erreur);
    afficherMessage('Réponse serveur invalide.', true);
  }
}

function nettoyerCadre(cadre) {
  if (cadre && cadre.parentNode) {
    cadre.onload = null;
    cadre.onerror = null;
    cadre.src = 'about:blank';
    setTimeout(() => cadre.parentNode.removeChild(cadre), 100);
  }
  etat.iframeActif = null;
}

function traiterReponse(donnees) {
  if (donnees.succes) {
    afficherMessage('Transfert réussi !');
    document.getElementById('apercu').src = donnees.url;
  } else {
    afficherMessage(donnees.message || 'Échec du transfert.', true);
  }
}

Traitement côté serveur avec PHP

Le script PHP gère le fichier uploadé, le déplace vers un répertoire sécurisé et renvoie une réponse JSON enveloppée dans un élément textarea pour éviter les problèmes d'interprétation de code (XSS).

<?php
header('Content-Type: text/plain; charset=utf-8');

$reponse = ['succes' => false, 'message' => '', 'url' => ''];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
    $fichier = $_FILES['image'];
    
    // Vérification des erreurs d'upload
    if ($fichier['error'] !== UPLOAD_ERR_OK) {
        $reponse['message'] = 'Code erreur upload: ' . $fichier['error'];
        envoyerReponse($reponse);
        exit;
    }
    
    // Validation du type de fichier
    $typesAutorises = ['image/jpeg', 'image/png', 'image/gif'];
    $typeDetecte = mime_content_type($fichier['tmp_name']);
    
    if (!in_array($typeDetecte, $typesAutorises)) {
        $reponse['message'] = 'Type de fichier non autorisé.';
        envoyerReponse($reponse);
        exit;
    }
    
    // Génération d'un nom de fichier unique
    $extension = pathinfo($fichier['name'], PATHINFO_EXTENSION);
    $nomUnique = uniqid('img_') . '.' . $extension;
    $cheminDestination = './uploads/' . $nomUnique;
    
    // Déplacement sécurisé du fichier temporaire
    if (move_uploaded_file($fichier['tmp_name'], $cheminDestination)) {
        $reponse['succes'] = true;
        $reponse['url'] = '/uploads/' . $nomUnique;
        $reponse['message'] = 'Upload réussi.';
    } else {
        $reponse['message'] = 'Erreur lors de l\'enregistrement.';
    }
} else {
    $reponse['message'] = 'Requête invalide.';
}

envoyerReponse($reponse);

function envoyerReponse($data) {
    $json = json_encode($data, JSON_UNESCAPED_UNICODE);
    echo '<textarea>' . htmlspecialchars($json, ENT_QUOTES, 'UTF-8') . '</textarea>';
}
?>

Prévisualisation locale de l'image

Pour afficher un aperçu immédiat après la sélection du fichier, on utilise l'API FileReader ou URL.createObjectURL(), avec une détection de support pour une compatibilité étendue.

function afficherApercuLocal(fichier) {
  const elementImg = document.getElementById('apercu');
  
  // Tentative avec l'API moderne Blob URL
  if (window.URL && URL.createObjectURL) {
    const urlBlob = URL.createObjectURL(fichier);
    elementImg.src = urlBlob;
    
    // Stocker l'URL pour libération ultérieure
    elementImg.dataset.urlBlob = urlBlob;
    elementImg.onload = function() {
      // Libérer la mémoire après chargement
      URL.revokeObjectURL(urlBlob);
      delete elementImg.dataset.urlBlob;
    };
  }
  // Fallback avec FileReader pour les anciens navigateurs
  else if (window.FileReader) {
    const lecteur = new FileReader();
    lecteur.onload = function(evenement) {
      elementImg.src = evenement.target.result;
    };
    lecteur.readAsDataURL(fichier);
  }
  else {
    afficherMessage('La prévisualisation n\'est pas supportée par votre navigateur.', true);
  }
}

Gestion des cas d'erreur et files d'attente

Un système robuste doit gérer les erreurs réseau, les dépassements de taille et contrôler le nombre d'uploads simultanés pour éviter la surcharge du navigateur.

// Gestionnaire de file d'attente pour contrôler la concurrence
class FileAttenteUpload {
  constructor(maximum = 3) {
    this.file = [];
    this.actifs = 0;
    this.maximum = maximum;
  }

  ajouter(tache) {
    this.file.push(tache);
    this.traiter();
  }

  traiter() {
    while (this.actifs < this.maximum && this.file.length > 0) {
      const tache = this.file.shift();
      this.actifs++;
      
      tache()
        .then(() => {
          this.actifs--;
          this.traiter();
        })
        .catch(() => {
          this.actifs--;
          this.traiter();
        });
    }
  }
}

// Initialisation et utilisation
const fileAttente = new FileAttenteUpload(2);

document.getElementById('btn-envoi').addEventListener('click', function() {
  fileAttente.ajouter(() => new Promise((resoudre) => {
    demarrerTeleversement();
    // La résolution se fera dans le callback de réponse
    window.resolveUpload = resoudre;
  }));
});

Considérations sur la compatibilité et la sécurité

Cette approche par iframe offre une compatibilité descendant jusqu'à Internet Explorer 6. Les points clés de sécurité incluent :

  • Toujours envelopper les réponses JSON dans un textarea côté serveur pour prévenir l'exécution de scripts.
  • Valider les types MIME et la taille des fichiers à la fois côté client et serveur.
  • Nettoyer systématiquement les éléments iframe du DOM après utilisation pour éviter les fuites mémoire.
  • Utiliser des noms de fichiers générés de manière imprévisible pour éviter les attaques par parcours de chemin.

Cette méthode constitue une solution de repli fiable lorsque les API modernes ne sont pas disponibles, tout en maintenant une expérience utilisateur acceptable.

Étiquettes: PHP iframe file-upload cross-browser-compatibility JavaScript

Publié le 10 juin à 16h29