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
|
odoo.define('auth_totp_portal.button', function (require) {
'use strict';
const {_t} = require('web.core');
const publicWidget = require('web.public.widget');
const Dialog = require('web.Dialog');
const {handleCheckIdentity} = require('portal.portal');
/**
* Replaces specific <field> elements by normal HTML, strip out the rest entirely
*/
function fromField(f, record) {
switch (f.getAttribute('name')) {
case 'qrcode':
const qrcode = document.createElement('img');
qrcode.setAttribute('class', 'img img-fluid offset-1');
qrcode.setAttribute('src', 'data:image/png;base64,' + record['qrcode']);
return qrcode;
case 'url':
const url = document.createElement('a');
url.setAttribute('href', record['url']);
url.textContent = f.getAttribute('text') || record['url'];
return url;
case 'code':
const code = document.createElement('input');
code.setAttribute('name', 'code');
code.setAttribute('class', 'form-control col-10 col-md-6');
code.setAttribute('placeholder', '6-digit code');
code.required = true;
code.maxLength = 6;
code.minLength = 6;
return code;
default: // just display the field's data
return document.createTextNode(record[f.getAttribute('name')] || '');
}
}
/**
* Apparently chrome literally absolutely can't handle parsing XML and using
* those nodes in an HTML document (even when parsing as application/xhtml+xml),
* this results in broken rendering and a number of things not working (e.g.
* classes) without any specific warning in the console or anything, things are
* just broken with no indication of why.
*
* So... rebuild the entire f'ing body using document.createElement to ensure
* we have HTML elements.
*
* This is a recursive implementation so it's not super efficient but the views
* to fixup *should* be relatively simple.
*/
function fixupViewBody(oldNode, record) {
let qrcode = null, code = null, node = null;
switch (oldNode.nodeType) {
case 1: // element
if (oldNode.tagName === 'field') {
node = fromField(oldNode, record);
switch (oldNode.getAttribute('name')) {
case 'qrcode':
qrcode = node;
break;
case 'code':
code = node;
break
}
break; // no need to recurse here
}
node = document.createElement(oldNode.tagName);
for(let i=0; i<oldNode.attributes.length; ++i) {
const attr = oldNode.attributes[i];
node.setAttribute(attr.name, attr.value);
}
for(let j=0; j<oldNode.childNodes.length; ++j) {
const [ch, qr, co] = fixupViewBody(oldNode.childNodes[j], record);
if (ch) { node.appendChild(ch); }
if (qr) { qrcode = qr; }
if (co) { code = co; }
}
break;
case 3: case 4: // text, cdata
node = document.createTextNode(oldNode.data);
break;
default:
// don't care about PI & al
}
return [node, qrcode, code]
}
/**
* Converts a backend <button> element and a bunch of metadata into a structure
* which can kinda be of use to Dialog.
*/
class Button {
constructor(parent, model, record_id, input_node, button_node) {
this._parent = parent;
this.model = model;
this.record_id = record_id;
this.input = input_node;
this.text = button_node.getAttribute('string');
this.classes = button_node.getAttribute('class') || null;
this.action = button_node.getAttribute('name');
if (button_node.getAttribute('special') === 'cancel') {
this.close = true;
this.click = null;
} else {
this.close = false;
// because Dialog doesnt' call() click on the descriptor object
this.click = this._click.bind(this);
}
}
async _click() {
if (!this.input.reportValidity()) {
this.input.classList.add('is-invalid');
return;
}
try {
await this.callAction(this.record_id, {code: this.input.value});
} catch (e) {
this.input.classList.add('is-invalid');
// show custom validity error message
this.input.setCustomValidity(e.message);
this.input.reportValidity();
return;
}
this.input.classList.remove('is-invalid');
// reloads page, avoid window.location.reload() because it re-posts forms
window.location = window.location;
}
async callAction(id, update) {
try {
await this._parent._rpc({model: this.model, method: 'write', args: [id, update]});
await handleCheckIdentity(
this._parent.proxy('_rpc'),
this._parent._rpc({model: this.model, method: this.action, args: [id]})
);
} catch(e) {
// avoid error toast (crashmanager)
e.event.preventDefault();
// try to unwrap mess of an error object to a usable error message
throw new Error(
!e.message ? e.toString()
: !e.message.data ? e.message.message
: e.message.data.message || _t("Operation failed for unknown reason.")
);
}
}
}
publicWidget.registry.TOTPButton = publicWidget.Widget.extend({
selector: '#auth_totp_portal_enable',
events: {
click: '_onClick',
},
async _onClick(e) {
e.preventDefault();
const w = await handleCheckIdentity(this.proxy('_rpc'), this._rpc({
model: 'res.users',
method: 'totp_enable_wizard',
args: [this.getSession().user_id]
}));
if (!w) {
// TOTP probably already enabled, just reload page
window.location = window.location;
return;
}
const {res_model: model, res_id: wizard_id} = w;
const record = await this._rpc({
model, method: 'read', args: [wizard_id, []]
}).then(ar => ar[0]);
const doc = new DOMParser().parseFromString(
document.getElementById('totp_wizard_view').textContent,
'application/xhtml+xml'
);
const xmlBody = doc.querySelector('sheet *');
const [body, , codeInput] = fixupViewBody(xmlBody, record);
// remove custom validity error message any time the field changes
// otherwise it sticks and browsers suppress submit
codeInput.addEventListener('input', () => codeInput.setCustomValidity(''));
const buttons = [];
for(const button of doc.querySelectorAll('footer button')) {
buttons.push(new Button(this, model, record.id, codeInput, button));
}
// wrap in a root host of .modal-body otherwise it breaks our neat flex layout
const $content = document.createElement('form');
$content.appendChild(body);
// implicit submission by pressing [return] from within input
$content.addEventListener('submit', (e) => {
e.preventDefault();
// sadness: footer not available as normal element
dialog.$footer.find('.btn-primary').click();
});
var dialog = new Dialog(this, {$content, buttons}).open();
}
});
publicWidget.registry.DisableTOTPButton = publicWidget.Widget.extend({
selector: '#auth_totp_portal_disable',
events: {
click: '_onClick'
},
async _onClick(e) {
e.preventDefault();
await handleCheckIdentity(
this.proxy('_rpc'),
this._rpc({model: 'res.users', method: 'totp_disable', args: [this.getSession().user_id]})
)
window.location = window.location;
}
});
});
|