I want to be able to download the PDF rendition of a Nuxeo document via CMIS using a single-page Web application. To be clear the Web application is not a Nuxeo application, just a basic app running on Apache, for example. The request involves a simple URL like this:
http://localhost:8080/nuxeo/json/cmis/default/root?succinct=true&streamId=nuxeo%3Arendition%3Apdf&cmisselector=content&objectId=07cdb579-b845-46a7-b22f-f49fa4f7de8b&download=attachment
You need to configure CORS in Nuxeo to allow CMIS requests from a Web app. I chose to restrict it to just the JSON binding from localhost. Here is my 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>
You'll need some sort of Web server to host the application. I used Apache.
- Open
authenticated-file-download.html
. - Fill in the parameters as needed.
- The document ID is required. There's one hard-coded in the JS but that's from my local server.
- Click the
Download
button.
Of course the request must be authenticated. In general I want to avoid use of the browser's ugly authentication dialog, so this means the authentication needs to occur in code. Thus a simple <a>
tag will not suffice. If the user clicks a link this navigates away from my single-page application, thus I lose any authentication info and the user gets the authentication dialog. The same problem applies to techniques that involve window.open
, using a <form>
, using an <iframe>
, etc.
In addition I want the user experience to be just like any other file download. Maybe it goes to the "Downloads" folder or even opens in the browser depending on the file type and browser support.
The solution is made of two parts:
- An XMLHTTPRequest (XHR) call to retrieve the file (as a blob); the call is authenticated.
- A Javascript library that takes the blob returned by XHR and saves it on the local system as a file.
To get the blob you just need to authenticate via XHR. The built-in params (username
and password
) for XHR don't seem to work (in fact they may not be intended to be used in this way). However manually adding the Authorization
header DOES work. E.g.:
xhr.setRequestHeader("Authorization", "Basic " + Base64.encode(userName + ":" + password));
To save the blob I used https://github.com/eligrey/FileSaver.js/. As far as I can decipher, here is what it does:
- Save the blob to temporary storage using the Javascript
File
API. - Create an
<a>
element and assign a blob URL to it that references the above file. - Enable the HTML5
download
attribute for this element.- This is how it works for Chrome and Firefox, but it depends on the browser. For example Safari does not yet support
download
.FileSaver.js
is robust enough to support several browsers.
- This is how it works for Chrome and Firefox, but it depends on the browser. For example Safari does not yet support
- "Click" the element via Javascript.
Note that FileSaver.js
implements the HTML5 W3C saveAs()
interface.
The solution is deceptively simple. It took me several days of research, understanding, and testing to find the answer. While scouring the internet I found there are several ways to "download a file" using Javascript and several ways to make authenticated requests using Javascript but I found literally nothing that combines the two on the client side. Every solution I found that attempted to combine the two involved modifications on the server side.
Caveat: the solution, in particular the file download, relies on the File
API from HTML5, as well as the download
attribute. The point is it works well mainly in modern browsers.
authenticated-file-download.html
- Here you can enter the necessary info to build a test URL and download the rendition.lib/authenticated-file-download.js
- The JS bits that make up the application.lib/base64.js
- Used to encode the user name and password for theAuthorization
header. It supposedly originated here but that gives a404
. I actually found it here.lib/FileSaver.js
- This library handles saving a file (e.g. the blob returned by XHR) to the local disk. From https://github.com/eligrey/FileSaver.js/.
-
I tried to make this example as simple as possible to focus on a) authenticating the request and b) saving the file. By no means is it meant to be a "best practice" example, just a simple explanation of how it works.
-
You need to configure CORS in Nuxeo to allow CMIS requests from a Web app. I chose to restrict it to just the JSON binding. Here is my 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 appears to be an older (but viable) solution. The reason I mention it is because it appears to skip the step of saving the file to local storage. It takes the blob returned by XHR and creates an anchor tag that points to it directly in memory (still using the blob URL syntax). But the behavior in Safari is far inferior to
FileSaver.js
. -
You can't access the filename using XHR once you're using CORS. There's a great explanation here. The summary is that you are limited to "simple response headers" when using XHR with CORS. Nuxeo returns the file name in the
content-disposition
header; this one is not accessible. It's no big deal when using CMIS because you can just get the document name via the CMIS object. -
Incidentally XHR's withCredentials does nothing and appears to have nothing to do with the username/password passed to XHR. It's used for passing cookies with CORS requests. CORS normally restricts/does not pass cookies across domains.