luci-base: luci.js: add LuCI.session.getToken()
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
index 5984ad184afc758dc627f397f367d771bac445b4..4b6894c080f1f1ed41fc74b3463d4af431f93209 100644 (file)
@@ -12,6 +12,8 @@
 (function(window, document, undefined) {
        'use strict';
 
+       var env = {};
+
        /* Object.assign polyfill for IE */
        if (typeof Object.assign !== 'function') {
                Object.defineProperty(Object, 'assign', {
                 */
                add: function(fn, interval) {
                        if (interval == null || interval <= 0)
-                               interval = window.L ? window.L.env.pollinterval : null;
+                               interval = env.pollinterval || null;
 
                        if (isNaN(interval) || typeof(fn) != 'function')
                                throw new TypeError('Invalid argument to LuCI.poll.add()');
         * To import the class in views, use `'require dom'`, to import it in
         * external JavaScript, use `L.require("dom").then(...)`.
         */
-       var DOM = Class.singleton(/* @lends LuCI.dom.prototype */ {
+       var DOM = Class.singleton(/** @lends LuCI.dom.prototype */ {
                __name__: 'LuCI.dom',
 
                /**
                 */
                bindClassInstance: function(node, inst) {
                        if (!(inst instanceof Class))
-                               L.error('TypeError', 'Argument must be a class instance');
+                               LuCI.prototype.error('TypeError', 'Argument must be a class instance');
 
                        return this.data(node, '_class', inst);
                },
                }
        });
 
+       /**
+        * @class session
+        * @memberof LuCI
+        * @hideconstructor
+        * @classdesc
+        *
+        * The `session` class provides various session related functionality.
+        */
+       var Session = Class.singleton(/** @lends LuCI.session.prototype */ {
+               __name__: 'LuCI.session',
+
+               /**
+                * Retrieve the current session ID.
+                *
+                * @returns {string}
+                * Returns the current session ID.
+                */
+               getID: function() {
+                       return env.sessionid || '00000000000000000000000000000000';
+               },
+
+               /**
+                * Retrieve the current session token.
+                *
+                * @returns {string|null}
+                * Returns the current session token or `null` if not logged in.
+                */
+               getToken: function() {
+                       return env.token || null;
+               },
+
+               /**
+                * Retrieve data from the local session storage.
+                *
+                * @param {string} [key]
+                * The key to retrieve from the session data store. If omitted, all
+                * session data will be returned.
+                *
+                * @returns {*}
+                * Returns the stored session data or `null` if the given key wasn't
+                * found.
+                */
+               getLocalData: function(key) {
+                       try {
+                               var sid = this.getID(),
+                                   item = 'luci-session-store',
+                                   data = JSON.parse(window.sessionStorage.getItem(item));
+
+                               if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
+                                       data = {};
+                                       data[sid] = {};
+                               }
+
+                               if (key != null)
+                                       return data[sid].hasOwnProperty(key) ? data[sid][key] : null;
+
+                               return data[sid];
+                       }
+                       catch (e) {
+                               return (key != null) ? null : {};
+                       }
+               },
+
+               /**
+                * Set data in the local session storage.
+                *
+                * @param {string} key
+                * The key to set in the session data store.
+                *
+                * @param {*} value
+                * The value to store. It will be internally converted to JSON before
+                * being put in the session store.
+                *
+                * @returns {boolean}
+                * Returns `true` if the data could be stored or `false` on error.
+                */
+               setLocalData: function(key, value) {
+                       if (key == null)
+                               return false;
+
+                       try {
+                               var sid = this.getID(),
+                                   item = 'luci-session-store',
+                                   data = JSON.parse(window.sessionStorage.getItem(item));
+
+                               if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
+                                       data = {};
+                                       data[sid] = {};
+                               }
+
+                               if (value != null)
+                                       data[sid][key] = value;
+                               else
+                                       delete data[sid][key];
+
+                               window.sessionStorage.setItem(item, JSON.stringify(data));
+
+                               return true;
+                       }
+                       catch (e) {
+                               return false;
+                       }
+               }
+       });
+
        /**
         * @class view
         * @memberof LuCI
         * The `view` class forms the basis of views and provides a standard
         * set of methods to inherit from.
         */
-       var View = Class.extend(/* @lends LuCI.view.prototype */ {
+       var View = Class.extend(/** @lends LuCI.view.prototype */ {
                __name__: 'LuCI.view',
 
                __init__: function() {
                        DOM.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
 
                        return Promise.resolve(this.load())
-                               .then(L.bind(this.render, this))
-                               .then(L.bind(function(nodes) {
+                               .then(LuCI.prototype.bind(this.render, this))
+                               .then(LuCI.prototype.bind(function(nodes) {
                                        var vp = document.getElementById('view');
 
                                        DOM.content(vp, nodes);
                                        DOM.append(vp, this.addFooter());
-                               }, this)).catch(L.error);
+                               }, this)).catch(LuCI.prototype.error);
                },
 
                /**
                 */
                handleSaveApply: function(ev, mode) {
                        return this.handleSave(ev).then(function() {
-                               L.ui.changes.apply(mode == '0');
+                               classes.ui.changes.apply(mode == '0');
                        });
                },
 
                 * methods are overwritten with `null`.
                 */
                addFooter: function() {
-                       var footer = E([]);
+                       var footer = E([]),
+                           vp = document.getElementById('view'),
+                           hasmap = false,
+                           readonly = true;
+
+                       vp.querySelectorAll('.cbi-map').forEach(function(map) {
+                               var m = DOM.findClassInstance(map);
+                               if (m) {
+                                       hasmap = true;
+
+                                       if (!m.readonly)
+                                               readonly = false;
+                               }
+                       });
 
-                       var saveApplyBtn = this.handleSaveApply ? new L.ui.ComboButton('0', {
+                       if (!hasmap)
+                               readonly = !LuCI.prototype.hasViewPermission();
+
+                       var saveApplyBtn = this.handleSaveApply ? new classes.ui.ComboButton('0', {
                                0: [ _('Save & Apply') ],
                                1: [ _('Apply unchecked') ]
                        }, {
                                        0: 'btn cbi-button cbi-button-apply important',
                                        1: 'btn cbi-button cbi-button-negative important'
                                },
-                               click: L.ui.createHandlerFn(this, 'handleSaveApply')
+                               click: classes.ui.createHandlerFn(this, 'handleSaveApply'),
+                               disabled: readonly || null
                        }).render() : E([]);
 
                        if (this.handleSaveApply || this.handleSave || this.handleReset) {
                                        saveApplyBtn, ' ',
                                        this.handleSave ? E('button', {
                                                'class': 'cbi-button cbi-button-save',
-                                               'click': L.ui.createHandlerFn(this, 'handleSave')
+                                               'click': classes.ui.createHandlerFn(this, 'handleSave'),
+                                               'disabled': readonly || null
                                        }, [ _('Save') ]) : '', ' ',
                                        this.handleReset ? E('button', {
                                                'class': 'cbi-button cbi-button-reset',
-                                               'click': L.ui.createHandlerFn(this, 'handleReset')
+                                               'click': classes.ui.createHandlerFn(this, 'handleReset'),
+                                               'disabled': readonly || null
                                        }, [ _('Reset') ]) : ''
                                ]));
                        }
            domParser = null,
            originalCBIInit = null,
            rpcBaseURL = null,
-           sysFeatures = null;
+           sysFeatures = null,
+           preloadClasses = null;
 
        /* "preload" builtin classes to make the available via require */
        var classes = {
                dom: DOM,
                poll: Poll,
                request: Request,
+               session: Session,
                view: View
        };
 
        var LuCI = Class.extend(/** @lends LuCI.prototype */ {
                __name__: 'LuCI',
-               __init__: function(env) {
+               __init__: function(setenv) {
 
                        document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
-                               if (env.base_url == null || env.base_url == '') {
+                               if (setenv.base_url == null || setenv.base_url == '') {
                                        var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
                                        if (m) {
-                                               env.base_url = m[1];
-                                               env.resource_version = m[2];
+                                               setenv.base_url = m[1];
+                                               setenv.resource_version = m[2];
                                        }
                                }
                        });
 
-                       if (env.base_url == null)
+                       if (setenv.base_url == null)
                                this.error('InternalError', 'Cannot find url of luci.js');
 
-                       env.cgi_base = env.scriptname.replace(/\/[^\/]+$/, '');
+                       setenv.cgi_base = setenv.scriptname.replace(/\/[^\/]+$/, '');
 
-                       Object.assign(this.env, env);
-
-                       document.addEventListener('poll-start', function(ev) {
-                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
-                                       e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
-                               });
-                       });
-
-                       document.addEventListener('poll-stop', function(ev) {
-                               document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
-                                       e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
-                               });
-                       });
+                       Object.assign(env, setenv);
 
                        var domReady = new Promise(function(resolveFn, rejectFn) {
                                document.addEventListener('DOMContentLoaded', resolveFn);
                 */
                error: function(type, fmt /*, ...*/) {
                        try {
-                               L.raise.apply(L, Array.prototype.slice.call(arguments));
+                               LuCI.prototype.raise.apply(LuCI.prototype,
+                                       Array.prototype.slice.call(arguments));
                        }
                        catch (e) {
                                if (!e.reported) {
-                                       if (L.ui)
-                                               L.ui.addNotification(e.name || _('Runtime error'),
+                                       if (classes.ui)
+                                               classes.ui.addNotification(e.name || _('Runtime error'),
                                                        E('pre', {}, e.message), 'danger');
                                        else
                                                DOM.content(document.querySelector('#maincontent'),
                        if (classes[name] != null) {
                                /* Circular dependency */
                                if (from.indexOf(name) != -1)
-                                       L.raise('DependencyError',
+                                       LuCI.prototype.raise('DependencyError',
                                                'Circular dependency: class "%s" depends on "%s"',
                                                name, from.join('" which depends on "'));
 
                                return Promise.resolve(classes[name]);
                        }
 
-                       url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
+                       url = '%s/%s.js%s'.format(env.base_url, name.replace(/\./g, '/'), (env.resource_version ? '?v=' + env.resource_version : ''));
                        from = [ name ].concat(from);
 
                        var compileClass = function(res) {
                                if (!res.ok)
-                                       L.raise('NetworkError',
+                                       LuCI.prototype.raise('NetworkError',
                                                'HTTP error %d while loading class file "%s"', res.status, url);
 
                                var source = res.text(),
 
                                                if (m) {
                                                        var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
-                                                       depends.push(L.require(dep, from));
+                                                       depends.push(LuCI.prototype.require(dep, from));
                                                        args += ', ' + as;
                                                }
                                                else if (!strictmatch.exec(s)) {
                                                                .format(args, source, res.url));
                                        }
                                        catch (error) {
-                                               L.raise('SyntaxError', '%s\n  in %s:%s',
+                                               LuCI.prototype.raise('SyntaxError', '%s\n  in %s:%s',
                                                        error.message, res.url, error.lineNumber || '?');
                                        }
 
                                        _class = _factory.apply(_factory, [window, document, L].concat(instances));
 
                                        if (!Class.isSubclass(_class))
-                                           L.error('TypeError', '"%s" factory yields invalid constructor', name);
+                                           LuCI.prototype.error('TypeError', '"%s" factory yields invalid constructor', name);
 
                                        if (_class.displayName == 'AnonymousClass')
                                                _class.displayName = toCamelCase(name + 'Class');
 
                /* DOM setup */
                probeRPCBaseURL: function() {
-                       if (rpcBaseURL == null) {
-                               try {
-                                       rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
-                               }
-                               catch (e) { }
-                       }
+                       if (rpcBaseURL == null)
+                               rpcBaseURL = Session.getLocalData('rpcBaseURL');
 
                        if (rpcBaseURL == null) {
                                var rpcFallbackURL = this.url('admin/ubus');
 
-                               rpcBaseURL = Request.get(this.env.ubuspath).then(function(res) {
-                                       return (rpcBaseURL = (res.status == 400) ? L.env.ubuspath : rpcFallbackURL);
+                               rpcBaseURL = Request.get(env.ubuspath).then(function(res) {
+                                       return (rpcBaseURL = (res.status == 400) ? env.ubuspath : rpcFallbackURL);
                                }, function() {
                                        return (rpcBaseURL = rpcFallbackURL);
                                }).then(function(url) {
-                                       try {
-                                               window.sessionStorage.setItem('rpcBaseURL', url);
-                                       }
-                                       catch (e) { }
-
+                                       Session.setLocalData('rpcBaseURL', url);
                                        return url;
                                });
                        }
                },
 
                probeSystemFeatures: function() {
-                       var sessionid = classes.rpc.getSessionID();
-
-                       if (sysFeatures == null) {
-                               try {
-                                       var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
-
-                                       if (this.isObject(data) && this.isObject(data[sessionid]))
-                                               sysFeatures = data[sessionid];
-                               }
-                               catch (e) {}
-                       }
+                       if (sysFeatures == null)
+                               sysFeatures = Session.getLocalData('features');
 
                        if (!this.isObject(sysFeatures)) {
                                sysFeatures = classes.rpc.declare({
                                        method: 'getFeatures',
                                        expect: { '': {} }
                                })().then(function(features) {
-                                       try {
-                                               var data = {};
-                                                   data[sessionid] = features;
-
-                                               window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
-                                       }
-                                       catch (e) {}
-
+                                       Session.setLocalData('features', features);
                                        sysFeatures = features;
 
                                        return features;
                        return Promise.resolve(sysFeatures);
                },
 
+               probePreloadClasses: function() {
+                       if (preloadClasses == null)
+                               preloadClasses = Session.getLocalData('preload');
+
+                       if (!Array.isArray(preloadClasses)) {
+                               preloadClasses = this.resolveDefault(classes.rpc.declare({
+                                       object: 'file',
+                                       method: 'list',
+                                       params: [ 'path' ],
+                                       expect: { 'entries': [] }
+                               })(this.fspath(this.resource('preload'))), []).then(function(entries) {
+                                       var classes = [];
+
+                                       for (var i = 0; i < entries.length; i++) {
+                                               if (entries[i].type != 'file')
+                                                       continue;
+
+                                               var m = entries[i].name.match(/(.+)\.js$/);
+
+                                               if (m)
+                                                       classes.push('preload.%s'.format(m[1]));
+                                       }
+
+                                       Session.setLocalData('preload', classes);
+                                       preloadClasses = classes;
+
+                                       return classes;
+                               });
+                       }
+
+                       return Promise.resolve(preloadClasses);
+               },
+
                /**
                 * Test whether a particular system feature is available, such as
                 * hostapd SAE support or an installed firewall. The features are
                notifySessionExpiry: function() {
                        Poll.stop();
 
-                       L.ui.showModal(_('Session expired'), [
+                       classes.ui.showModal(_('Session expired'), [
                                E('div', { class: 'alert-message warning' },
                                        _('A new login is required since the authentication session expired.')),
                                E('div', { class: 'right' },
                                        }, _('To login…')))
                        ]);
 
-                       L.raise('SessionError', 'Login session is expired');
+                       LuCI.prototype.raise('SessionError', 'Login session is expired');
                },
 
                /* private */
                        rpcClass.setBaseURL(rpcBaseURL);
 
                        rpcClass.addInterceptor(function(msg, req) {
-                               if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
+                               if (!LuCI.prototype.isObject(msg) ||
+                                   !LuCI.prototype.isObject(msg.error) ||
+                                   msg.error.code != -32002)
                                        return;
 
-                               if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+                               if (!LuCI.prototype.isObject(req) ||
+                                   (req.object == 'session' && req.method == 'access'))
                                        return;
 
                                return rpcClass.declare({
                                        'method': 'access',
                                        'params': [ 'scope', 'object', 'function' ],
                                        'expect': { access: true }
-                               })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
+                               })('uci', 'luci', 'read').catch(LuCI.prototype.notifySessionExpiry);
                        });
 
                        Request.addInterceptor(function(res) {
                                if (!isDenied)
                                        return;
 
-                               L.notifySessionExpiry();
+                               LuCI.prototype.notifySessionExpiry();
+                       });
+
+                       document.addEventListener('poll-start', function(ev) {
+                               uiClass.showIndicator('poll-status', _('Refreshing'), function(ev) {
+                                       Request.poll.active() ? Request.poll.stop() : Request.poll.start();
+                               });
+                       });
+
+                       document.addEventListener('poll-stop', function(ev) {
+                               uiClass.showIndicator('poll-status', _('Paused'), null, 'inactive');
                        });
 
-                       return this.probeSystemFeatures().finally(this.initDOM);
+                       return Promise.all([
+                               this.probeSystemFeatures(),
+                               this.probePreloadClasses()
+                       ]).finally(LuCI.prototype.bind(function() {
+                               var tasks = [];
+
+                               if (Array.isArray(preloadClasses))
+                                       for (var i = 0; i < preloadClasses.length; i++)
+                                               tasks.push(this.require(preloadClasses[i]));
+
+                               return Promise.all(tasks);
+                       }, this)).finally(this.initDOM);
                },
 
                /* private */
                 * @instance
                 * @memberof LuCI
                 */
-               env: {},
+               env: env,
+
+               /**
+                * Construct an absolute filesystem path relative to the server
+                * document root.
+                *
+                * @instance
+                * @memberof LuCI
+                *
+                * @param {...string} [parts]
+                * An array of parts to join into a path.
+                *
+                * @return {string}
+                * Return the joined path.
+                */
+               fspath: function(/* ... */) {
+                       var path = env.documentroot;
+
+                       for (var i = 0; i < arguments.length; i++)
+                               path += '/' + arguments[i];
+
+                       var p = path.replace(/\/+$/, '').replace(/\/+/g, '/').split(/\//),
+                           res = [];
+
+                       for (var i = 0; i < p.length; i++)
+                               if (p[i] == '..')
+                                       res.pop();
+                               else if (p[i] != '.')
+                                       res.push(p[i]);
+
+                       return res.join('/');
+               },
 
                /**
                 * Construct a relative URL path from the given prefix and parts.
                 * Returns the resulting URL path.
                 */
                url: function() {
-                       return this.path(this.env.scriptname, arguments);
+                       return this.path(env.scriptname, arguments);
                },
 
                /**
                 * Returns the resulting URL path.
                 */
                resource: function() {
-                       return this.path(this.env.resource, arguments);
+                       return this.path(env.resource, arguments);
                },
 
                /**
                 * Returns the resulting URL path.
                 */
                media: function() {
-                       return this.path(this.env.media, arguments);
+                       return this.path(env.media, arguments);
                },
 
                /**
                 * Returns the URL path to the current view.
                 */
                location: function() {
-                       return this.path(this.env.scriptname, this.env.requestpath);
+                       return this.path(env.scriptname, env.requestpath);
                },
 
 
                 */
                poll: function(interval, url, args, cb, post) {
                        if (interval !== null && interval <= 0)
-                               interval = this.env.pollinterval;
+                               interval = env.pollinterval;
 
-                       var data = post ? { token: this.env.token } : null,
+                       var data = post ? { token: env.token } : null,
                            method = post ? 'POST' : 'GET';
 
                        if (!/^(?:\/|\S+:\/\/)/.test(url))
                                        });
                },
 
+               /**
+                * Check whether a view has sufficient permissions.
+                *
+                * @return {boolean|null}
+                * Returns `null` if the current session has no permission at all to
+                * load resources required by the view. Returns `false` if readonly
+                * permissions are granted or `true` if at least one required ACL
+                * group is granted with write permissions.
+                */
+               hasViewPermission: function() {
+                       if (!this.isObject(env.nodespec) || !env.nodespec.satisfied)
+                           return null;
+
+                       return !env.nodespec.readonly;
+               },
+
                /**
                 * Deprecated wrapper around {@link LuCI.poll.remove Poll.remove()}.
                 *
                 */
                get: function(url, data, callback, timeout) {
                        this.active = true;
-                       L.get(url, data, this._response.bind(this, callback), timeout);
+                       LuCI.prototype.get(url, data, this._response.bind(this, callback), timeout);
                },
 
                /**
                 */
                post: function(url, data, callback, timeout) {
                        this.active = true;
-                       L.post(url, data, this._response.bind(this, callback), timeout);
+                       LuCI.prototype.post(url, data, this._response.bind(this, callback), timeout);
                },
 
                /**
                 * Throws an `InternalError` with the message `Not implemented`
                 * when invoked.
                 */
-               send_form: function() { L.error('InternalError', 'Not implemented') },
+               send_form: function() { LuCI.prototype.error('InternalError', 'Not implemented') },
        });
 
-       XHR.get = function() { return window.L.get.apply(window.L, arguments) };
-       XHR.post = function() { return window.L.post.apply(window.L, arguments) };
-       XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
+       XHR.get = function() { return LuCI.prototype.get.apply(LuCI.prototype, arguments) };
+       XHR.post = function() { return LuCI.prototype.post.apply(LuCI.prototype, arguments) };
+       XHR.poll = function() { return LuCI.prototype.poll.apply(LuCI.prototype, arguments) };
        XHR.stop = Request.poll.remove.bind(Request.poll);
        XHR.halt = Request.poll.stop.bind(Request.poll);
        XHR.run = Request.poll.start.bind(Request.poll);