luci-base: xhr.js: rework class, handle expired session
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / xhr.js
1 /*
2 * xhr.js - XMLHttpRequest helper class
3 * (c) 2008-2018 Jo-Philipp Wich <jo@mein.io>
4 */
5
6 XHR.prototype = {
7 _encode: function(obj) {
8 obj = obj ? obj : { };
9 obj['_'] = Math.random();
10
11 if (typeof obj == 'object') {
12 var code = '';
13 var self = this;
14
15 for (var k in obj)
16 code += (code ? '&' : '') +
17 k + '=' + encodeURIComponent(obj[k]);
18
19 return code;
20 }
21
22 return obj;
23 },
24
25 _response: function(callback, ts) {
26 if (this._xmlHttp.readyState !== 4)
27 return;
28
29 var status = this._xmlHttp.status,
30 login = this._xmlHttp.getResponseHeader("X-LuCI-Login-Required"),
31 type = this._xmlHttp.getResponseHeader("Content-Type"),
32 json = null;
33
34 if (status === 403 && login === 'yes') {
35 XHR.halt();
36
37 showModal(_('Session expired'), [
38 E('div', { class: 'alert-message warning' },
39 _('A new login is required since the authentication session expired.')),
40 E('div', { class: 'right' },
41 E('div', {
42 class: 'btn primary',
43 click: function() {
44 var loc = window.location;
45 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
46 }
47 }, _('To login…')))
48 ]);
49 }
50 else if (type && type.toLowerCase().match(/^application\/json\b/)) {
51 try {
52 json = JSON.parse(this._xmlHttp.responseText);
53 }
54 catch(e) {
55 json = null;
56 }
57 }
58
59 callback(this._xmlHttp, json, Date.now() - ts);
60 },
61
62 busy: function() {
63 if (!this._xmlHttp)
64 return false;
65
66 switch (this._xmlHttp.readyState)
67 {
68 case 1:
69 case 2:
70 case 3:
71 return true;
72
73 default:
74 return false;
75 }
76 },
77
78 abort: function() {
79 if (this.busy())
80 this._xmlHttp.abort();
81 },
82
83 get: function(url, data, callback, timeout) {
84 this._xmlHttp = new XMLHttpRequest();
85
86 var xhr = this._xmlHttp,
87 code = this._encode(data);
88
89 url = location.protocol + '//' + location.host + url;
90
91 if (code)
92 if (url.substr(url.length-1,1) == '&')
93 url += code;
94 else
95 url += '?' + code;
96
97 xhr.open('GET', url, true);
98
99 if (!isNaN(timeout))
100 xhr.timeout = timeout;
101
102 xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
103 xhr.send(null);
104 },
105
106 post: function(url, data, callback, timeout) {
107 this._xmlHttp = new XMLHttpRequest();
108
109 var xhr = this._xmlHttp,
110 code = this._encode(data);
111
112 xhr.open('POST', url, true);
113
114 if (!isNaN(timeout))
115 xhr.timeout = timeout;
116
117 xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
118 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
119 xhr.send(code);
120 },
121
122 cancel: function() {
123 this._xmlHttp.onreadystatechange = function() {};
124 this._xmlHttp.abort();
125 },
126
127 send_form: function(form, callback, extra_values) {
128 var code = '';
129
130 for (var i = 0; i < form.elements.length; i++) {
131 var e = form.elements[i];
132
133 if (e.options) {
134 code += (code ? '&' : '') +
135 form.elements[i].name + '=' + encodeURIComponent(
136 e.options[e.selectedIndex].value
137 );
138 }
139 else if (e.length) {
140 for (var j = 0; j < e.length; j++)
141 if (e[j].name) {
142 code += (code ? '&' : '') +
143 e[j].name + '=' + encodeURIComponent(e[j].value);
144 }
145 }
146 else {
147 code += (code ? '&' : '') +
148 e.name + '=' + encodeURIComponent(e.value);
149 }
150 }
151
152 if (typeof extra_values == 'object')
153 for (var key in extra_values)
154 code += (code ? '&' : '') +
155 key + '=' + encodeURIComponent(extra_values[key]);
156
157 return (form.method == 'get'
158 ? this.get(form.getAttribute('action'), code, callback)
159 : this.post(form.getAttribute('action'), code, callback));
160 }
161 }
162
163 XHR.get = function(url, data, callback) {
164 (new XHR()).get(url, data, callback);
165 }
166
167 XHR.post = function(url, data, callback) {
168 (new XHR()).post(url, data, callback);
169 }
170
171 XHR.poll = function(interval, url, data, callback, post) {
172 if (isNaN(interval) || interval < 1)
173 interval = 5;
174
175 if (!XHR._q) {
176 XHR._t = 0;
177 XHR._q = [ ];
178 XHR._r = function() {
179 for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
180 {
181 if (!(XHR._t % e.interval) && !e.xhr.busy())
182 e.xhr[post ? 'post' : 'get'](e.url, e.data, e.callback, e.interval * 1000 * 5 - 5);
183 }
184
185 XHR._t++;
186 };
187 }
188
189 var e = {
190 interval: interval,
191 callback: callback,
192 url: url,
193 data: data,
194 xhr: new XHR()
195 };
196
197 XHR._q.push(e);
198
199 return e;
200 }
201
202 XHR.stop = function(e) {
203 for (var i = 0; XHR._q && XHR._q[i]; i++) {
204 if (XHR._q[i] === e) {
205 e.xhr.cancel();
206 XHR._q.splice(i, 1);
207 return true;
208 }
209 }
210
211 return false;
212 }
213
214 XHR.halt = function() {
215 if (XHR._i) {
216 /* show & set poll indicator */
217 try {
218 document.getElementById('xhr_poll_status').style.display = '';
219 document.getElementById('xhr_poll_status_on').style.display = 'none';
220 document.getElementById('xhr_poll_status_off').style.display = '';
221 } catch(e) { }
222
223 window.clearInterval(XHR._i);
224 XHR._i = null;
225 }
226 }
227
228 XHR.run = function() {
229 if (XHR._r && !XHR._i) {
230 /* show & set poll indicator */
231 try {
232 document.getElementById('xhr_poll_status').style.display = '';
233 document.getElementById('xhr_poll_status_on').style.display = '';
234 document.getElementById('xhr_poll_status_off').style.display = 'none';
235 } catch(e) { }
236
237 /* kick first round manually to prevent one second lag when setting up
238 * the poll interval */
239 XHR._r();
240 XHR._i = window.setInterval(XHR._r, 1000);
241 }
242 }
243
244 XHR.running = function() {
245 return !!(XHR._r && XHR._i);
246 }
247
248 function XHR() {}
249
250 document.addEventListener('DOMContentLoaded', XHR.run);