Not long ago, I presented a webinar on CMIS where I demonstrated a standalone Web application that used CMIS to access content from a Nuxeo repository. One particular feature I was excited to show was the ability to download a rendition of a document living in the Nuxeo Platform.
Much to my embarrassment I discovered later that this example did not work at all unless I was already logged in to the Nuxeo application. Kind of defeats the purpose of building a standalone web application!
Note: I will be blogging about my CMIS client in a future post as well.
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:
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 the code. Thus a simple
<a> tag will not suffice. If the user clicks a link, this navigates away from the single-page application, thus any authentication information is lost and the user gets the browser’s authentication dialog. The same problem applies to techniques that involve
window.open, using a
<form>, using an
Save the File Locally
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 main parts:
- An XMLHTTPRequest (XHR) call to retrieve the file (as a blob); the call is authenticated.
To get the blob you just need to authenticate via XHR. The built-in params (
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. For example:
xhr.setRequestHeader("Authorization", "Basic " + Base64.encode(userName + ":" + password));
To save the blob I used
As far as I can decipher, here is what it does:
- Create an
<a>element and assign a blob URL to it that references the above file.
- Enable the HTML5
downloadattribute` for this element.
Note: This is how it works for Chrome and Firefox, but it depends on the browser. For example Safari does not yet support
FileSaver.js is robust enough to support several browsers.
FileSaver.js implements the HTML5 W3C
saveAs() interface. At some point it's likely the browsers will implementsaveAs()` natively.
Caveat: the solution, in particular the file download, relies on the
FileAPI from HTML5, as well as the
download attribute. The point is it works well mainly in modern browsers.
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>
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 filename 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 XHRs 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.
I tried to make the 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.
Check out the example applicationhere!