Microsoft Internet Explorer et les fichiers CSV générés par PHP

Je ne sais pas si je l'ai mentionné, mais dans la vraie vie j'ai un vrai métier, et ce vrai métier implique (entre autres choses passionante) pas mal de développement sur-mesure en PHP. Aujourd'hui un client m'a fait (re)découvrir une triste vérité : dans la vie, il y a les standards, il y a Microsoft Internet Explorer, et l'intersection entre les deux ensemble est pour ainsi dire inexistante. Pourtant je ne lui parlais pas de haute technologie, je lui parlais de fichiers CSV (comma-separated values), un format bien plus vieux que moi.

Le contexte est assez simple : soit une plateforme d'e-learning (basée sur le logiciel libre Chamilo), le commanditaire souhaitait pouvoir fournir au client final un moyen simple d'obtenir les données de suivi de ses apprenants dans un format pratique. Nous avons donc convenu des champs de données à fournir, de leur format, et d'un modus operandi. Le plus simple pour le client étant de disposer d'une URL lui fournissant directement un fichier CSV avec des données à jour, j'ai concoté un script PHP qui va chercher à la demande les données qu'il faut, les formatte en CSV, et les sert en tant que tel au client. En pratique, le client ouvre une URL dans son navigateur, le fichier est téléchargé, et comme c'est bien fait le navigateur lui propose directement d'ouvrir son fichier dans Excel (ou équivalent). Pratique.

Pour ce faire la recette est simple : les en-têtes HTTP permettent de définir un Content-type donc de dire clairement au navigateur "ce que je te donne là, c'est des données de tel type, pas du texte brut dénué de sens". Un petit tour dans les textes normatifs me disent que le MIME-type pour du CSV est text/mime, j'ai donc codé la fonction suivante :

function serve_as_csv($csvdata) {
    $date = date("Y-m-d");
    $filename = "tracking-$date.csv";
    header("Content-length: ".strlen($csvdata));
    header("Content-type: text/csv");
    header("Content-Disposition: attachment; filename=$filename");
    ob_start();
    echo $csvdata;
    ob_end_flush();
    exit;
}

Jusqu'ici tout va bien, tous les navigateurs s'en sortent. Sauf IE. Ce brave petit n'est pas foutu de reconnaître un MIME-type défini il y a une quinzaine d'années, on n'est pas sorti de l'auberge. La documentation de Microsoft conseille de le servir en tant que application/download mais évidemment ça ne marche pas plus. On ne va quand même pas imaginer que de la documentation Microsoft pour contourner un défaut d'un logiciel Microsoft soit fiable ...

J'ai donc du passer en revue tous les MIME-types plus ou moins délirants qui pourraient vaguement être associés à un CSV. Aucun n'est bon. On vise à côté, il faut d'abord expliquer à cette andouille comment on gère du cache, ensuite on voit. Arrive donc la combinaison gagnante:

function serve_as_csv($csvdata) {
    $date = date("Y-m-d");
    $filename = "tracking-$date.csv";
    header("Content-length: ".strlen($csvdata));
    header("Pragma: must-revalidate");
    header("Cache-Control: must-revalidate");
    header("Content-type: application/vnd.ms-excel");
    header("Content-Disposition: attachment; filename=$filename");
    ob_start();
    echo $csvdata;
    ob_end_flush();
    exit;
}

Là ça fonctionne avec Internet Explorer. Le souci vient par contre pour le reste du monde, qui ne sait absolument pas que faire de ce application/vnd.ms-excel défini dans aucune RFC et du coup propose tout au plus de télécharger le fichier, en tout cas pas de l'ouvrir avec un logiciel aapproprié. Génial. Donc il faut effectuer une vérification sur le User-Agent, quelque chose du goût de :

if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) {
    // Ceci n'est pas un navigateur
}

Mais pour une raison qui doit certainement ne regarder que le jardinier du sultan du Brunei, Opera se déclare comme "Compatible MSIE" donc ce check est borké, puisqu'Opera ne sait pas trop (à juste titre) quoi faire de application/vnd.ms-excel, ce n'est pas assez. Mais ce qui est bon c'est que ce n'est que la dernière particularité foireuse à prendre en compte. Donc, au bout de 30 minutes d'essais-erreurs, ça, ça marche :

function serve_as_csv($csvdata) {
    $date = date("Y-m-d");
    $filename = "tracking-$date.csv";
    header("Content-length: ".strlen($csvdata));
    if ((strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false) && (strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') == false)) { 
        header("Pragma: must-revalidate");
        header("Cache-Control: must-revalidate");
        header("Content-type: application/vnd.ms-excel");
    }
    else {
        header("Content-type: text/csv");
    }
    header("Content-Disposition: attachment; filename=$filename");
    ob_start();
    echo $csvdata;
    ob_end_flush();
    exit;
}

Pfiou ! Donc voilà, ici pour la postérité.

code (php) | fail | msie | workaround | wtf

<<< Où l'auteur annonce qu'il publie ailleurs (déjà) (2010-06-02) | Quit Facebook Day (2010-05-31) >>>