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
textareacô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.