diff options
| author | Xavier Morel <xmo@odoo.com> | 2018-07-12 10:19:37 +0200 |
|---|---|---|
| committer | Xavier Morel <xmo@odoo.com> | 2018-07-13 17:31:22 +0200 |
| commit | 35d452cffbb1554c70f0acba85dfc4ef472f544c (patch) | |
| tree | b08e860394367234966e7e99b078370ed2645735 /addons/web/static/src/js/core | |
| parent | ccbfa94d32158b8df383df54f3eced38e602e5ef (diff) | |
[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
Diffstat (limited to 'addons/web/static/src/js/core')
| -rw-r--r-- | addons/web/static/src/js/core/ajax.js | 175 |
1 files changed, 72 insertions, 103 deletions
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 = $('<div>'); - - var complete = function () { - if (options.complete) { options.complete(); } - clearTimeout(timer); - $form_data.remove(); - $target.remove(); - if (remove_form && $form) { $form.remove(); } - }; - var $target = $('<iframe style="display: none;">') - .attr({id: id, name: id}) - .appendTo(document.body) - .load(function () { - try { - if (options.error) { - var body = this.contentDocument.body; - var nodes = body.children.length === 0 ? body.childNodes : body.children; - var errorParams = {}; - - try { // Case of a serialized Odoo Exception: It is Json Parsable - var node = nodes[1] || nodes[0]; - errorParams = JSON.parse(node.textContent); - } catch (e) { // Arbitrary uncaught python side exception - errorParams = { - message: nodes.length > 1 ? nodes[1].textContent : '', - data: { - title: nodes.length > 0 ? nodes[0].textContent : '', - } - } - } - options.error(errorParams); - } - } finally { - complete(); - } - }); + var xhr = new XMLHttpRequest(); + var data; if (options.form) { - $form = $(options.form); + xhr.open(options.form.method, options.form.action); + data = new FormData(options.form); } else { - remove_form = true; - $form = $('<form>', { - action: options.url, - method: 'POST' - }).appendTo(document.body); + xhr.open('POST', options.url); + data = new FormData(); + _.each(options.data || {}, function (v, k) { + data.append(k, v) + }); } + data.append('token', 'dummy-because-api-expects-one'); if (core.csrf_token) { - $('<input type="hidden" name="csrf_token">') - .val(core.csrf_token) - .appendTo($form_data); + data.append('csrf_token', core.csrf_token); } + // IE11 wants this after xhr.open or it throws + xhr.responseType = 'blob'; + + // onreadystatechange[readyState = 4] + // => onload (success) | onerror (error) | onabort + // => onloadend + xhr.onload = function () { + var mimetype = xhr.response.type; + if (xhr.status === 200 && mimetype !== 'text/html') { + // replace because apparently we send some C-D headers with a trailing ";" + // todo: maybe a lack of CD[attachment] should be interpreted as an error case? + var header = (xhr.getResponseHeader('Content-Disposition') || '').replace(/;$/, ''); + var filename = header ? contentdisposition.parse(header).parameters.filename : null; + + download(xhr.response, filename, mimetype); + // not sure download is going to be sync so this may be called + // before the file is actually fetched (?) + if (options.success) { options.success(); } + return true; + } - var hparams = _.extend({}, options.data || {}, {token: token}); - _.each(hparams, function (value, key) { - var $input = $form.find('[name=' + key +']'); - if (!$input.length) { - $input = $('<input type="hidden" name="' + key + '">') - .appendTo($form_data); + if (!options.error) { + return true; + } + var decoder = new FileReader(); + decoder.onload = function () { + var contents = decoder.result; + + var err; + var doc = new DOMParser().parseFromString(contents, 'text/html'); + var nodes = doc.body.children.length === 0 ? doc.body.childNodes : doc.body.children; + try { // Case of a serialized Odoo Exception: It is Json Parsable + var node = nodes[1] || nodes[0]; + err = JSON.parse(node.textContent); + } catch (e) { // Arbitrary uncaught python side exception + err = { + message: nodes.length > 1 ? nodes[1].textContent : '', + data: { + name: String(xhr.status), + title: nodes.length > 0 ? nodes[0].textContent : '', + } + } } - $input.val(value); - }); - - $form - .append($form_data) - .attr('target', id) - .get(0).submit(); - - var waitLoop = function () { - var cookies = document.cookie.split(';'); - // setup next check - timer = setTimeout(waitLoop, CHECK_INTERVAL); - for (var i=0; i<cookies.length; ++i) { - var cookie = cookies[i].replace(/^\s*/, ''); - if (!cookie.indexOf(cookie_name === 0)) { continue; } - var cookie_val = cookie.substring(cookie_length + 1); - if (parseInt(cookie_val, 10) !== token) { continue; } - - // clear cookie - document.cookie = _.str.sprintf("%s=;expires=%s;path=/", - cookie_name, new Date().toGMTString()); - if (options.success) { options.success(); } - complete(); - return; + options.error(err); + }; + decoder.readAsText(xhr.response); + }; + xhr.onerror = function () { + if (options.error) { + options.error({ + message: _("Something happened while trying to contact the server, check that the server is online and that you still have a working network connection."), + data: { title: _t("Could not connect to the server") } + }); } }; - timer = setTimeout(waitLoop, CHECK_INTERVAL); + if (options.complete) { + xhr.onloadend = function () { options.complete(); } + } + + xhr.send(data); return true; -}; +} function post (controller_url, data) { |
