1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
|
odoo.define('web.ErrorDialogRegistry', function (require) {
"use strict";
var Registry = require('web.Registry');
return new Registry();
});
odoo.define('web.CrashManager', function (require) {
"use strict";
const AbstractService = require('web.AbstractService');
var ajax = require('web.ajax');
const BrowserDetection = require('web.BrowserDetection');
var core = require('web.core');
var Dialog = require('web.Dialog');
var ErrorDialogRegistry = require('web.ErrorDialogRegistry');
var Widget = require('web.Widget');
var _t = core._t;
var _lt = core._lt;
// Register this eventlistener before qunit does.
// Some errors needs to be negated by the crash_manager.
window.addEventListener('unhandledrejection', ev =>
core.bus.trigger('crash_manager_unhandledrejection', ev)
);
let active = true;
/**
* An extension of Dialog Widget to render the warnings and errors on the website.
* Extend it with your template of choice like ErrorDialog/WarningDialog
*/
var CrashManagerDialog = Dialog.extend({
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
['/web/static/src/xml/crash_manager.xml']
),
/**
* @param {Object} error
* @param {string} error.message the message in Warning/Error Dialog
* @param {string} error.traceback the traceback in ErrorDialog
*
* @constructor
*/
init: function (parent, options, error) {
this._super.apply(this, [parent, options]);
this.message = error.message;
this.traceback = error.traceback;
core.bus.off('close_dialogs', this);
},
});
var ErrorDialog = CrashManagerDialog.extend({
template: 'CrashManager.error',
});
var WarningDialog = CrashManagerDialog.extend({
template: 'CrashManager.warning',
/**
* Sets size to medium by default.
*
* @override
*/
init: function (parent, options, error) {
this._super(parent, _.extend({
size: 'medium',
}, options), error);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Focuses the ok button.
*
* @override
*/
open: function () {
this._super({shouldFocusButtons: true});
},
});
var CrashManager = AbstractService.extend({
init: function () {
var self = this;
active = true;
this.isConnected = true;
this.odooExceptionTitleMap = {
'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"),
'odoo.exceptions.AccessDenied': _lt("Access Denied"),
'odoo.exceptions.AccessError': _lt("Access Error"),
'odoo.exceptions.MissingError': _lt("Missing Record"),
'odoo.exceptions.UserError': _lt("User Error"),
'odoo.exceptions.ValidationError': _lt("Validation Error"),
'odoo.exceptions.Warning': _lt("Warning"),
};
this.browserDetection = new BrowserDetection();
this._super.apply(this, arguments);
// crash manager integration
core.bus.on('rpc_error', this, this.rpc_error);
window.onerror = function (message, file, line, col, error) {
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
// To enable onerror to work properly with CORS file, you should:
// 1. add crossorigin="anonymous" to your <script> tag loading the file
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
// Since in some case it wont be possible to to this, this handle should have the possibility to be
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
// If it is not handled, we should display something clearer than the common crash_manager error dialog
// since it won't show anything except "Script error."
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
if (!file && !line && !col) {
// Chrome and Opera set "Script error." on the `message` and hide the `error`
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
if (window.onOriginError) {
window.onOriginError();
delete window.onOriginError;
} else {
self.show_error({
type: _t("Odoo Client Error"),
message: _t("Unknown CORS error"),
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
});
}
} else {
// ignore Chrome video internal error: https://crbug.com/809574
if (!error && message === 'ResizeObserver loop limit exceeded') {
return;
}
var traceback = error ? error.stack : '';
self.show_error({
type: _t("Odoo Client Error"),
message: message,
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback},
});
}
};
// listen to unhandled rejected promises, and throw an error when the
// promise has been rejected due to a crash
core.bus.on('crash_manager_unhandledrejection', this, function (ev) {
if (ev.reason && ev.reason instanceof Error) {
// Error.prototype.stack is non-standard.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
// However, most engines provide an implementation.
// In particular, Chrome formats the contents of Error.stack
// https://v8.dev/docs/stack-trace-api#compatibility
let traceback;
if (self.browserDetection.isBrowserChrome()) {
traceback = ev.reason.stack;
} else {
traceback = `${_t("Error:")} ${ev.reason.message}\n${ev.reason.stack}`;
}
self.show_error({
type: _t("Odoo Client Error"),
message: '',
data: {debug: _t('Traceback:') + "\n" + traceback},
});
} else {
// the rejection is not due to an Error, so prevent the browser
// from displaying an 'unhandledrejection' error in the console
ev.stopPropagation();
ev.stopImmediatePropagation();
ev.preventDefault();
}
});
},
enable: function () {
active = true;
},
disable: function () {
active = false;
},
handleLostConnection: function () {
var self = this;
if (!this.isConnected) {
// already handled, nothing to do. This can happen when several
// rpcs are done in parallel and fail because of a lost connection.
return;
}
this.isConnected = false;
var delay = 2000;
core.bus.trigger('connection_lost');
setTimeout(function checkConnection() {
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
core.bus.trigger('connection_restored');
self.isConnected = true;
}).guardedCatch(function () {
// exponential backoff, with some jitter
delay = (delay * 1.5) + 500*Math.random();
setTimeout(checkConnection, delay);
});
}, delay);
},
rpc_error: function(error) {
// Some qunit tests produces errors before the DOM is set.
// This produces an error loop as the modal/toast has no DOM to attach to.
if (!document.body || !active || this.connection_lost) return;
// Connection lost error
if (error.code === -32098) {
this.handleLostConnection();
return;
}
// Special exception handlers, see crash_registry bellow
var handler = core.crash_registry.get(error.data.name, true);
if (handler) {
new (handler)(this, error).display();
return;
}
// Odoo custom exception: UserError, AccessError, ...
if (_.has(this.odooExceptionTitleMap, error.data.name)) {
error = _.extend({}, error, {
data: _.extend({}, error.data, {
message: error.data.arguments[0],
title: this.odooExceptionTitleMap[error.data.name],
}),
});
this.show_warning(error);
return;
}
// Any other Python exception
this.show_error(error);
},
show_warning: function (error, options) {
if (!active) {
return;
}
var message = error.data ? error.data.message : error.message;
var title = _t("Something went wrong !");
if (error.type) {
title = _.str.capitalize(error.type);
} else if (error.data && error.data.title) {
title = _.str.capitalize(error.data.title);
}
return this._displayWarning(message, title, options);
},
show_error: function (error) {
if (!active) {
return;
}
error.traceback = error.data.debug;
var dialogClass = error.data.context && ErrorDialogRegistry.get(error.data.context.exception_class) || ErrorDialog;
var dialog = new dialogClass(this, {
title: _.str.capitalize(error.type) || _t("Odoo Error"),
}, error);
// When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
var $clipboardBtn;
var clipboard;
dialog.opened(function () {
// When the full traceback is shown, scroll it to the end (useful for better python error reporting)
dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) {
e.target.scrollTop = e.target.scrollHeight;
});
$clipboardBtn = dialog.$(".o_clipboard_button");
$clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
clipboard = new window.ClipboardJS($clipboardBtn[0], {
text: function () {
return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim();
},
// Container added because of Bootstrap modal that give the focus to another element.
// We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
// https://github.com/zenorocha/clipboard.js/issues/155
container: dialog.el,
});
clipboard.on("success", function (e) {
_.defer(function () {
$clipboardBtn.tooltip("show");
_.delay(function () {
$clipboardBtn.tooltip("hide");
}, 800);
});
});
});
dialog.on("closed", this, function () {
$clipboardBtn.tooltip('dispose');
clipboard.destroy();
});
return dialog.open();
},
show_message: function(exception) {
return this.show_error({
type: _t("Odoo Client Error"),
message: exception,
data: {debug: ""}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} message
* @param {string} title
* @param {Object} options
*/
_displayWarning: function (message, title, options) {
return new WarningDialog(this, Object.assign({}, options, {
title,
}), {
message,
}).open();
},
});
/**
* An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
*/
var ExceptionHandler = {
/**
* @param parent The parent.
* @param error The error object as returned by the JSON-RPC implementation.
*/
init: function(parent, error) {},
/**
* Called to inform to display the widget, if necessary. A typical way would be to implement
* this interface in a class extending instance.web.Dialog and simply display the dialog in this
* method.
*/
display: function() {},
};
/**
* Handle redirection warnings, which behave more or less like a regular
* warning, with an additional redirection button.
*/
var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
init: function(parent, error) {
this._super(parent);
this.error = error;
},
display: function() {
var self = this;
var error = this.error;
var additional_context = _.extend({}, this.context, error.data.arguments[3]);
new WarningDialog(this, {
title: _.str.capitalize(error.type) || _t("Odoo Warning"),
buttons: [
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
self.do_action(
error.data.arguments[1],
{
additional_context: additional_context,
});
self.destroy();
}, close: true},
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
]
}, {
message: error.data.arguments[0],
}).open();
}
});
core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
function session_expired(cm) {
return {
display: function () {
const notif = {
type: _t("Odoo Session Expired"),
message: _t("Your Odoo session expired. The current page is about to be refreshed."),
};
const options = {
buttons: [{
text: _t("Ok"),
click: () => window.location.reload(true),
close: true
}],
};
cm.show_warning(notif, options);
}
};
}
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
core.crash_registry.add('504', function (cm) {
return {
display: function () {
cm.show_warning({
type: _t("Request timeout"),
message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
}
};
});
return {
CrashManager: CrashManager,
ErrorDialog: ErrorDialog,
WarningDialog: WarningDialog,
disable: () => active = false,
};
});
|