J’ai présenté il n’y a pas si longtemps un webinar dans lequel j’ai fait la démonstration d’une application Web autonome utilisant CMIS pour accéder à du contenu stocké dans une base documentaire Nuxeo. L’une des fonctionnalités que j’avais le plus envie de montrer était la possibilité de télécharger une version générée d’un document présent dans Nuxeo Platform. Quel embarras quand j’ai découvert plus tard que c’était impossible de présenter cet exemple sans être connecté au préalable à l’application Nuxeo. Ça gâche un peu l’intérêt de concevoir une application Web autonome !

Je me suis donc mis au travail pour réparer cet oubli. Il s’est avéré que c’était un problème assez compliqué, mais qui disposait d’une réponse assez simple. Puisque le problème n’est pas spécifique à Nuxeo, j’ai décidé de le traiter dans un article séparé. Ces informations devraient être utiles pour toutes les personnes ayant besoin de télécharger des fichiers qui nécessitent une authentification à partir de code JavaScript.

Remarque : je parlerai de mon client CMIS dans un prochain post.

Scénario utilisateur

Je veux pouvoir télécharger la version PDF d’un document Nuxeo via CMIS en utilisant une application Web monopage. Pour être clair, l’application Web n’est pas une application Nuxeo, juste une application basique tournant, par exemple, sous Apache. La requête implique une URL simple comme celle-ci : http://localhost:8080/nuxeo/json/cmis/default/root?succinct=true&streamId=nuxeo%3Arendition%3Apdf&cmisselector=content&objectId=07cdb579-b845-46a7-b22f-f49fa4f7de8b&download=attachment

Le problème

Authentification

Bien sûr, la requête doit être authentifiée. En général, j’essaye de ne pas utiliser l’horrible boîte de dialogue du navigateur pour l’authentification, ce qui veut dire que celle-ci doit être gérée dans le code. Par conséquent, une simple balise <a> n’est pas suffisante. Si l’utilisateur clique sur un lien, celle-ci l’envoie hors de l’application monopage, perdant ainsi toutes les informations d’authentification et déclenchant la boîte de dialogue du navigateur pour l’utilisateur. Le même problème se présente pour les techniques impliquant window.open, utilisant un <form>, un <iframe>, etc.

Enregistrer le fichier localement

Par ailleurs, je veux que l’utilisateur ait l’impression de réaliser un téléchargement classique. Par exemple que le fichier aille dans le répertoire « Téléchargements », ou qu’il s’ouvre même dans le navigateur en fonction du type de fichier et du navigateur.

La solution

La solution est composée de deux parties principales :

  • Un appel XMLHTTPRequest (XHR) pour récupérer le fichier (en tant que blob) ; l’appel est authentifié.
  • Une bibliothèque JavaScript qui prend le blob renvoyé par XHR et l’enregistre dans le système local en tant que fichier.

Authenticated File Download

L’explication

Il suffit d’être authentifié via XHR pour récupérer le blob. Les paramètres intégrés (username et password) pour XHR n’ont pas l’air de fonctionner (ils ne sont probablement pas faits pour fonctionner de cette façon). Cependant, il est possible d’ajouter l’en-tête Authorization, et ça fonctionne. Par exemple : xhr.setRequestHeader("Authorization", "Basic " + Base64.encode(userName + ":" + password)); Pour enregistrer le blob, j’ai utilisé FileSaver.js. De ce que je comprends, voici ce qu’il fait :

  • Enregistrement du blob vers un stockage temporaire à l’aide de l’API File JavaScript.
  • Création d’un élément <a> et affectation d’une URL de blob à cet élément qui référence le fichier ci-dessus.
  • Activation de l’attribut HTML5 download pour cet élément.
    • Remarque : c’est valable pour Chrome et Firefox, mais tout dépend du navigateur. Par exemple, Safari ne supporte pas encore download. FileSaver.js est assez robuste pour supporter plusieurs navigateurs.
  • « Clic » sur l’élément via JavaScript.

Notez que FileSaver.js implémente l’interface HTML5 W3C saveAs(). Il est probable que les navigateurs finissent par implémenter saveAs() nativement.

Le contexte

La solution a l’air bien plus simple qu’il n’y parait. Il m’a fallu plusieurs jours de recherche, de compréhension et de test pour trouver la réponse. En cherchant sur Internet, j’ai trouvé plusieurs façons de « télécharger un fichier » à l’aide de JavaScript et plusieurs façons d’effectuer des requêtes authentifiées à l’aide de JavaScript, mais je n’ai rien trouvé qui combinait les deux actions côté client. Toutes les solutions qui essayaient de combiner les deux impliquaient d’effectuer des modifications côté serveur.

Avertissement : la solution, en particulier le téléchargement de fichier, s’appuie sur l’API File de HTML5, ainsi que sur l’attribut download. Le fait est que ça fonctionne bien, principalement dans les navigateurs modernes.

Conseils

  • Il faut configurer CORS dans Nuxeo pour autoriser les requêtes CMIS depuis une application Web. J’ai choisi de le limiter à la liaison JSON à partir de localhost. Voici ma contribution :
<extension target="org.nuxeo.ecm.platform.web.common.requestcontroller.service.RequestControllerService" point="corsConfig">
<corsConfig name="forCMISBrowserBinding" allowOrigin="http://localhost">
 <pattern>/nuxeo/json/cmis.*</pattern>
</corsConfig>
</extension>
  • download.js semble être une solution plus datée (mais viable). J’en parle parce qu’on dirait qu’elle évite l’étape de sauvegarde du fichier dans un stockage local. Elle prend le blob renvoyé par XHR et crée une balise d’ancrage qui pointe directement vers celui-ci dans la mémoire (toujours en utilisant la syntaxe URL du blob). Mais le comportement dans Safari est bien moins stable qu’avec FileSaver.js.

  • Il est impossible d’accéder au nom du fichier en utilisant XHR une fois que l’on utilise CORS. Vous trouverez une bonne explication ici. En résumé, c’est parce qu’on est limité aux « en-têtes de réponse simples » lors de l’utilisation de XHR avec CORS. Nuxeo renvoie le nom du fichier dans l’en-tête content-disposition qui n’est pas accessible. Ça ne pose pas de réel problème lorsque l’on utilise CMIS car il est possible d’obtenir le nom du document en passant par l’objet CMIS.

  • Accessoirement, les XHR avec identifiants ne font rien et semblent n’avoir rien à voir avec le nom d’utilisateur / mot de passe transmis à XHR. C’est utilisé pour transmettre les cookies avec les requêtes CORS. En temps normal, CORS restreint / ne transfère par les cookies entre les domaines.

L’exemple

J’ai essayé de rendre mon exemple aussi simple que possible pour se concentrer sur a) l’authentification de la requête, et b) l’enregistrement du fichier. Ce n’est en aucun cas un exemple de bonne pratique, mais juste une explication simple du fonctionnement.

Jetez un œil à l’application d’exemple ici !