From 35d452cffbb1554c70f0acba85dfc4ef472f544c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 12 Jul 2018 10:19:37 +0200 Subject: [PATCH] [IMP] web: replace iframe-based get_file by XHR The error handling in get_file has always been somewhat problematic (sarcasm ho!). Pending PR #24854 attempts to add special handling for gateway timeout aside from its base stuff, and that's really quite terrible due to get_file's limitation of only being able to read the content of the document loaded in the iframe (no proper access to status codes & al). That was the occasion to look up modern API and see if we could do file downloads in JS without iframe crap. Turns out we can: xhr2 is well supported so we can POST, get a blob back, and create a "local download" for that in-memory blob using either a[download] (most browsers) or msSaveBlob (IE10+, that's actually a convenient-to-great API and it's a shame there's nothing like it in the standard). The changes are not trivial but save ~30LOC and most of the overhead is replicating the old/weird error handling as that requires additionally decoding the blob to a string & parsing that string to a document before being able to get the content (whereas before we'd just access to the document loaded in the iframe). OTOH we can now properly forward non-200 status codes to the crash manager (as name) which will allow using regular CM hooks to provide custom warning and error messages. Note that this requires two libraries: * a stripped down version of the content-disposition NPM lib, as download file names are provided via that header and parsing the header is decidedly non-trivial * the download.js library as it handles the various edge cases from different browsers, I'm don't think all of it is necessary for our use case but the library is not that big so no real point in trying to strip it out and potentially break it in the process --- addons/web/static/src/js/core/ajax.js | 177 +++++-------- .../static/src/js/libs/content-disposition.js | 249 ++++++++++++++++++ addons/web/static/src/js/libs/download.js | 153 +++++++++++ addons/web/views/webclient_templates.xml | 2 + 4 files changed, 477 insertions(+), 104 deletions(-) create mode 100644 addons/web/static/src/js/libs/content-disposition.js create mode 100644 addons/web/static/src/js/libs/download.js diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index fa79c65ef45..81643e99430 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -4,6 +4,10 @@ odoo.define('web.ajax', function (require) { var core = require('web.core'); var utils = require('web.utils'); var time = require('web.time'); +var download = require('web.download'); +var contentdisposition = require('web.contentdisposition'); + +var _t = core._t; function genericJsonRpc (fct_name, params, settings, fct) { var shadow = settings.shadow || false; @@ -269,120 +273,85 @@ var loadJS = (function () { * changed to make it work. */ function get_file(options) { - // need to detect when the file is done downloading (not used - // yet, but we'll need it to fix the UI e.g. with a throbber - // while dump is being generated), iframe load event only fires - // when the iframe content loads, so we need to go smarter: - // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx - var timer, token = new Date().getTime(), - cookie_name = 'fileToken', cookie_length = cookie_name.length, - CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'), - remove_form = false; - - - // iOS devices doesn't allow iframe use the way we do it, - // opening a new window seems the best way to workaround - if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) { - var params = _.extend({}, options.data || {}, {token: token}); - var url = options.session.url(options.url, params); - if (options.complete) { options.complete(); } - - var w = window.open(url); - if (!w || w.closed || typeof w.closed === 'undefined') { - // popup was blocked - return false; - } - return true; - } - - var $form, $form_data = $('
'); - - var complete = function () { - if (options.complete) { options.complete(); } - clearTimeout(timer); - $form_data.remove(); - $target.remove(); - if (remove_form && $form) { $form.remove(); } - }; - var $target = $('