luci-base: xhr.js: rework class, handle expired session
authorJo-Philipp Wich <jo@mein.io>
Fri, 16 Nov 2018 18:31:07 +0000 (19:31 +0100)
committerJo-Philipp Wich <jo@mein.io>
Fri, 16 Nov 2018 20:11:34 +0000 (21:11 +0100)
Drop very old IE compat code, restructure class, align code style with
other files and properly handle JSON mimetypes with charset trailer.

Also detect session related 403 errors and show a modal prompting
to re-login.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/xhr.js

index 25a90e7254ffbcccb203c55e068b7dc8840ca0b2..8292fcdb6ce6d94b60f212fa082c99752aa68ea8 100644 (file)
@@ -1,24 +1,65 @@
 /*
  * xhr.js - XMLHttpRequest helper class
- * (c) 2008-2010 Jo-Philipp Wich
+ * (c) 2008-2018 Jo-Philipp Wich <jo@mein.io>
  */
 
-XHR = function()
-{
-       this.reinit = function()
-       {
-               if (window.XMLHttpRequest) {
-                       this._xmlHttp = new XMLHttpRequest();
+XHR.prototype = {
+       _encode: function(obj) {
+               obj = obj ? obj : { };
+               obj['_'] = Math.random();
+
+               if (typeof obj == 'object') {
+                       var code = '';
+                       var self = this;
+
+                       for (var k in obj)
+                               code += (code ? '&' : '') +
+                                       k + '=' + encodeURIComponent(obj[k]);
+
+                       return code;
                }
-               else if (window.ActiveXObject) {
-                       this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
+
+               return obj;
+       },
+
+       _response: function(callback, ts) {
+               if (this._xmlHttp.readyState !== 4)
+                       return;
+
+               var status = this._xmlHttp.status,
+                   login = this._xmlHttp.getResponseHeader("X-LuCI-Login-Required"),
+                   type = this._xmlHttp.getResponseHeader("Content-Type"),
+                   json = null;
+
+               if (status === 403 && login === 'yes') {
+                       XHR.halt();
+
+                       showModal(_('Session expired'), [
+                               E('div', { class: 'alert-message warning' },
+                                       _('A new login is required since the authentication session expired.')),
+                               E('div', { class: 'right' },
+                                       E('div', {
+                                               class: 'btn primary',
+                                               click: function() {
+                                                       var loc = window.location;
+                                                       window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
+                                               }
+                                       }, _('To login…')))
+                       ]);
                }
-               else {
-                       alert("xhr.js: XMLHttpRequest is not supported by this browser!");
+               else if (type && type.toLowerCase().match(/^application\/json\b/)) {
+                       try {
+                               json = JSON.parse(this._xmlHttp.responseText);
+                       }
+                       catch(e) {
+                               json = null;
+                       }
                }
-       }
 
-       this.busy = function() {
+               callback(this._xmlHttp, json, Date.now() - ts);
+       },
+
+       busy: function() {
                if (!this._xmlHttp)
                        return false;
 
@@ -32,20 +73,18 @@ XHR = function()
                        default:
                                return false;
                }
-       }
+       },
 
-       this.abort = function() {
+       abort: function() {
                if (this.busy())
                        this._xmlHttp.abort();
-       }
+       },
 
-       this.get = function(url,data,callback,timeout)
-       {
-               this.reinit();
+       get: function(url, data, callback, timeout) {
+               this._xmlHttp = new XMLHttpRequest();
 
-               var ts   = Date.now();
-               var xhr  = this._xmlHttp;
-               var code = this._encode(data);
+               var xhr = this._xmlHttp,
+                   code = this._encode(data);
 
                url = location.protocol + '//' + location.host + url;
 
@@ -60,83 +99,51 @@ XHR = function()
                if (!isNaN(timeout))
                        xhr.timeout = timeout;
 
-               xhr.onreadystatechange = function()
-               {
-                       if (xhr.readyState == 4) {
-                               var json = null;
-                               if (xhr.getResponseHeader("Content-Type") == "application/json") {
-                                       try { json = JSON.parse(xhr.responseText); }
-                                       catch(e) { json = null; }
-                               }
-
-                               callback(xhr, json, Date.now() - ts);
-                       }
-               }
-
+               xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
                xhr.send(null);
-       }
-
-       this.post = function(url,data,callback,timeout)
-       {
-               this.reinit();
+       },
 
-               var ts   = Date.now();
-               var xhr  = this._xmlHttp;
-               var code = this._encode(data);
+       post: function(url, data, callback, timeout) {
+               this._xmlHttp = new XMLHttpRequest();
 
-               xhr.onreadystatechange = function()
-               {
-                       if (xhr.readyState == 4) {
-                               var json = null;
-                               if (xhr.getResponseHeader("Content-Type") == "application/json") {
-                                       try { json = JSON.parse(xhr.responseText); }
-                                       catch(e) { json = null; }
-                               }
-
-                               callback(xhr, json, Date.now() - ts);
-                       }
-               }
+               var xhr = this._xmlHttp,
+                   code = this._encode(data);
 
                xhr.open('POST', url, true);
 
                if (!isNaN(timeout))
                        xhr.timeout = timeout;
 
+               xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
                xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                xhr.send(code);
-       }
+       },
 
-       this.cancel = function()
-       {
-               this._xmlHttp.onreadystatechange = function(){};
+       cancel: function() {
+               this._xmlHttp.onreadystatechange = function() {};
                this._xmlHttp.abort();
-       }
+       },
 
-       this.send_form = function(form,callback,extra_values)
-       {
+       send_form: function(form, callback, extra_values) {
                var code = '';
 
-               for (var i = 0; i < form.elements.length; i++)
-               {
+               for (var i = 0; i < form.elements.length; i++) {
                        var e = form.elements[i];
 
-                       if (e.options)
-                       {
+                       if (e.options) {
                                code += (code ? '&' : '') +
                                        form.elements[i].name + '=' + encodeURIComponent(
                                                e.options[e.selectedIndex].value
                                        );
                        }
-                       else if (e.length)
-                       {
+                       else if (e.length) {
                                for (var j = 0; j < e.length; j++)
                                        if (e[j].name) {
                                                code += (code ? '&' : '') +
                                                        e[j].name + '=' + encodeURIComponent(e[j].value);
                                        }
                        }
-                       else
-                       {
+                       else {
                                code += (code ? '&' : '') +
                                        e.name + '=' + encodeURIComponent(e.value);
                        }
@@ -147,46 +154,25 @@ XHR = function()
                                code += (code ? '&' : '') +
                                        key + '=' + encodeURIComponent(extra_values[key]);
 
-               return(
-                       (form.method == 'get')
-                               ? this.get(form.getAttribute('action'), code, callback)
-                               : this.post(form.getAttribute('action'), code, callback)
-               );
-       }
-
-       this._encode = function(obj)
-       {
-               obj = obj ? obj : { };
-               obj['_'] = Math.random();
-
-               if (typeof obj == 'object')
-               {
-                       var code = '';
-                       var self = this;
-
-                       for (var k in obj)
-                               code += (code ? '&' : '') +
-                                       k + '=' + encodeURIComponent(obj[k]);
-
-                       return code;
-               }
-
-               return obj;
+               return (form.method == 'get'
+                       ? this.get(form.getAttribute('action'), code, callback)
+                       : this.post(form.getAttribute('action'), code, callback));
        }
 }
 
-XHR.get = function(url, data, callback)
-{
+XHR.get = function(url, data, callback) {
        (new XHR()).get(url, data, callback);
 }
 
-XHR.poll = function(interval, url, data, callback, post)
-{
+XHR.post = function(url, data, callback) {
+       (new XHR()).post(url, data, callback);
+}
+
+XHR.poll = function(interval, url, data, callback, post) {
        if (isNaN(interval) || interval < 1)
                interval = 5;
 
-       if (!XHR._q)
-       {
+       if (!XHR._q) {
                XHR._t = 0;
                XHR._q = [ ];
                XHR._r = function() {
@@ -213,8 +199,7 @@ XHR.poll = function(interval, url, data, callback, post)
        return e;
 }
 
-XHR.stop = function(e)
-{
+XHR.stop = function(e) {
        for (var i = 0; XHR._q && XHR._q[i]; i++) {
                if (XHR._q[i] === e) {
                        e.xhr.cancel();
@@ -226,10 +211,8 @@ XHR.stop = function(e)
        return false;
 }
 
-XHR.halt = function()
-{
-       if (XHR._i)
-       {
+XHR.halt = function() {
+       if (XHR._i) {
                /* show & set poll indicator */
                try {
                        document.getElementById('xhr_poll_status').style.display = '';
@@ -242,10 +225,8 @@ XHR.halt = function()
        }
 }
 
-XHR.run = function()
-{
-       if (XHR._r && !XHR._i)
-       {
+XHR.run = function() {
+       if (XHR._r && !XHR._i) {
                /* show & set poll indicator */
                try {
                        document.getElementById('xhr_poll_status').style.display = '';
@@ -260,9 +241,10 @@ XHR.run = function()
        }
 }
 
-XHR.running = function()
-{
+XHR.running = function() {
        return !!(XHR._r && XHR._i);
 }
 
+function XHR() {}
+
 document.addEventListener('DOMContentLoaded', XHR.run);