luci-mod-network: wireless.js: restore client disconnect functionality
[oweals/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / wireless.js
index 595cd1e221d25473f5c94c78916a575fb580cf92..61838a23631c5d841656818427e32b431abf01f9 100644 (file)
@@ -1,4 +1,6 @@
 'use strict';
+'require fs';
+'require ui';
 'require rpc';
 'require uci';
 'require form';
@@ -7,7 +9,7 @@
 'require tools.widgets as widgets';
 
 function count_changes(section_id) {
-       var changes = L.ui.changes.changes, n = 0;
+       var changes = ui.changes.changes, n = 0;
 
        if (!L.isObject(changes))
                return n;
@@ -89,19 +91,20 @@ function render_network_status(radioNet) {
            channel = radioNet.getChannel(),
            disabled = (radioNet.get('disabled') == '1' || uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == '1'),
            is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled),
+           is_mesh = (radioNet.getMode() == 'mesh'),
            changecount = count_changes(radioNet.getName()),
            status_text = null;
 
        if (changecount)
                status_text = E('a', {
                        href: '#',
-                       click: L.bind(L.ui.changes.displayChanges, L.ui.changes)
+                       click: L.bind(ui.changes.displayChanges, ui.changes)
                }, _('Interface has %d pending changes').format(changecount));
        else if (!is_assoc)
                status_text = E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated'));
 
        return L.itemlist(E('div'), [
-               _('SSID'),       radioNet.getSSID() || '?',
+               is_mesh ? _('Mesh ID') : _('SSID'), (is_mesh ? radioNet.getMeshID() : radioNet.getSSID()) || '?',
                _('Mode'),       mode,
                _('BSSID'),      (!changecount && is_assoc) ? bssid : null,
                _('Encryption'), (!changecount && is_assoc) ? radioNet.getActiveEncryption() || _('None') : null,
@@ -194,7 +197,7 @@ function network_updown(id, map, ev) {
        }
 
        return map.save().then(function() {
-               L.ui.changes.apply()
+               ui.changes.apply()
        });
 }
 
@@ -225,7 +228,7 @@ var CBIWifiFrequencyValue = form.Value.extend({
                                '11a': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : []
                        };
 
-                       for (var i = 0; Array.isArray(data[1]) && i < data[1].length; i++)
+                       for (var i = 0; i < data[1].length; i++)
                                this.channels[(data[1][i].mhz > 2484) ? '11a' : '11g'].push(
                                        data[1][i].channel,
                                        '%d (%d Mhz)'.format(data[1][i].channel, data[1][i].mhz),
@@ -285,7 +288,7 @@ var CBIWifiFrequencyValue = form.Value.extend({
                        if (vals[i+2])
                                sel.add(E('option', { value: vals[i+0] }, [ vals[i+1] ]));
 
-               if (!isNaN(vals.selected))
+               if (vals && !isNaN(vals.selected))
                        sel.selectedIndex = vals.selected;
 
                sel.parentNode.style.display = (sel.options.length <= 1) ? 'none' : '';
@@ -482,7 +485,7 @@ var CBIWifiCountryValue = form.Value.extend({
        },
 
        renderWidget: function(section_id, option_index, cfgvalue) {
-               var typeClass = this.keylist.length ? form.ListValue : form.Value;
+               var typeClass = (this.keylist && this.keylist.length) ? form.ListValue : form.Value;
                return typeClass.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
        }
 });
@@ -517,7 +520,7 @@ return L.view.extend({
                        btns[2].disabled = busy;
                }
 
-               var table = document.querySelector('wifi_assoclist_table'),
+               var table = document.querySelector('#wifi_assoclist_table'),
                    hosts = data[0],
                    trows = [];
 
@@ -527,7 +530,16 @@ return L.view.extend({
                            ipv4 = hosts.getIPAddrByMACAddr(bss.mac),
                            ipv6 = hosts.getIP6AddrByMACAddr(bss.mac);
 
-                       trows.push([
+                       var hint;
+
+                       if (name && ipv4 && ipv6)
+                               hint = '%s (%s, %s)'.format(name, ipv4, ipv6);
+                       else if (name && (ipv4 || ipv6))
+                               hint = '%s (%s)'.format(name, ipv4 || ipv6);
+                       else
+                               hint = name || ipv4 || ipv6 || '?';
+
+                       var row = [
                                E('span', { 'class': 'ifacebadge' }, [
                                        E('img', {
                                                'src': L.resource('icons/wifi%s.png').format(bss.network.isUp() ? '' : '_disabled'),
@@ -537,17 +549,39 @@ return L.view.extend({
                                        E('small', '(%s)'.format(bss.network.getIfname()))
                                ]),
                                bss.mac,
-                               name ? '%s (%s)'.format(name, ipv4 || ipv6 || '?') : ipv4 || ipv6 || '?',
+                               hint,
                                render_signal_badge(Math.min((bss.signal + 110) / 70 * 100, 100), bss.signal, bss.noise),
                                E('span', {}, [
                                        E('span', format_wifirate(bss.rx)),
                                        E('br'),
                                        E('span', format_wifirate(bss.tx))
                                ])
-                       ]);
+                       ];
+
+                       if (bss.network.isClientDisconnectSupported()) {
+                               if (table.firstElementChild.childNodes.length < 6)
+                                       table.firstElementChild.appendChild(E('div', { 'class': 'th nowrap right'}, [ _('Disconnect') ]));
+
+                               row.push(E('button', {
+                                       'class': 'cbi-button cbi-button-remove',
+                                       'click': L.bind(function(net, mac, ev) {
+                                               L.dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
+                                               ev.currentTarget.classList.add('spinning');
+                                               ev.currentTarget.disabled = true;
+                                               ev.currentTarget.blur();
+
+                                               net.disconnectClient(mac, true, 5, 60000);
+                                       }, this, bss.network, bss.mac)
+                               }, [ _('Disconnect') ]));
+                       }
+                       else {
+                               row.push('-');
+                       }
+
+                       trows.push(row);
                }
 
-               cbi_update_table('#wifi_assoclist_table', trows, E('em', _('No information available')));
+               cbi_update_table(table, trows, E('em', _('No information available')));
 
                var stat = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large');
 
@@ -603,18 +637,18 @@ return L.view.extend({
                }
 
                return Promise.all(tasks)
-                       .then(L.bind(L.ui.changes.init, L.ui.changes))
-                       .then(L.bind(L.ui.changes.apply, L.ui.changes));
+                       .then(L.bind(ui.changes.init, ui.changes))
+                       .then(L.bind(ui.changes.apply, ui.changes));
        },
 
        renderMigration: function() {
-               L.ui.showModal(_('Wireless configuration migration'), [
+               ui.showModal(_('Wireless configuration migration'), [
                        E('p', _('The existing wireless configuration needs to be changed for LuCI to function properly.')),
                        E('p', _('Upon pressing "Continue", anonymous "wifi-iface" sections will be assigned with a name in the form <em>wifinet#</em> and the network will be restarted to apply the updated configuration.')),
                        E('div', { 'class': 'right' },
                                E('button', {
                                        'class': 'btn cbi-button-action important',
-                                       'click': L.ui.createHandlerFn(this, 'handleMigration')
+                                       'click': ui.createHandlerFn(this, 'handleMigration')
                                }, _('Continue')))
                ]);
        },
@@ -689,17 +723,17 @@ return L.view.extend({
                                        E('button', {
                                                'class': 'cbi-button cbi-button-neutral',
                                                'title': _('Restart radio interface'),
-                                               'click': L.ui.createHandlerFn(this, radio_restart, section_id)
+                                               'click': ui.createHandlerFn(this, radio_restart, section_id)
                                        }, _('Restart')),
                                        E('button', {
                                                'class': 'cbi-button cbi-button-action important',
                                                'title': _('Find and join network'),
-                                               'click': L.ui.createHandlerFn(this, 'handleScan', inst)
+                                               'click': ui.createHandlerFn(this, 'handleScan', inst)
                                        }, _('Scan')),
                                        E('button', {
                                                'class': 'cbi-button cbi-button-add',
                                                'title': _('Provide new network'),
-                                               'click': L.ui.createHandlerFn(this, 'handleAdd', inst)
+                                               'click': ui.createHandlerFn(this, 'handleAdd', inst)
                                        }, _('Add'))
                                ];
                        }
@@ -711,17 +745,17 @@ return L.view.extend({
                                        E('button', {
                                                'class': 'cbi-button cbi-button-neutral enable-disable',
                                                'title': isDisabled ? _('Enable this network') : _('Disable this network'),
-                                               'click': L.ui.createHandlerFn(this, network_updown, section_id, this.map)
+                                               'click': ui.createHandlerFn(this, network_updown, section_id, this.map)
                                        }, isDisabled ? _('Enable') : _('Disable')),
                                        E('button', {
                                                'class': 'cbi-button cbi-button-action important',
                                                'title': _('Edit this network'),
-                                               'click': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
+                                               'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
                                        }, _('Edit')),
                                        E('button', {
                                                'class': 'cbi-button cbi-button-negative remove',
                                                'title': _('Delete this network'),
-                                               'click': L.ui.createHandlerFn(this, 'handleRemove', section_id)
+                                               'click': ui.createHandlerFn(this, 'handleRemove', section_id)
                                        }, _('Remove'))
                                ];
                        }
@@ -741,7 +775,8 @@ return L.view.extend({
                                ss.tab('general', _('General Setup'));
                                ss.tab('advanced', _('Advanced Settings'));
 
-                               var isDisabled = (radioNet.get('disabled') == '1');
+                               var isDisabled = (radioNet.get('disabled') == '1' ||
+                                       uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == 1);
 
                                o = ss.taboption('general', form.DummyValue, '_wifistat_modal', _('Status'));
                                o.cfgvalue = L.bind(function(radioNet) {
@@ -752,7 +787,7 @@ return L.view.extend({
                                o = ss.taboption('general', form.Button, '_toggle', isDisabled ? _('Wireless network is disabled') : _('Wireless network is enabled'));
                                o.inputstyle = isDisabled ? 'apply' : 'reset';
                                o.inputtitle = isDisabled ? _('Enable') : _('Disable');
-                               o.onclick = L.ui.createHandlerFn(s, network_updown, s.section, s.map);
+                               o.onclick = ui.createHandlerFn(s, network_updown, s.section, s.map);
 
                                o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '<br />' + _('Operating frequency'));
                                o.ucisection = s.section;
@@ -1046,9 +1081,7 @@ return L.view.extend({
                                };
 
 
-                               encr.value('none', _('No Encryption'));
-                               encr.value('wep-open',   _('WEP Open System'));
-                               encr.value('wep-shared', _('WEP Shared Key'));
+                               var crypto_modes = [];
 
                                if (hwtype == 'mac80211') {
                                        var has_supplicant = L.hasSystemFeature('wpasupplicant'),
@@ -1068,26 +1101,26 @@ return L.view.extend({
 
 
                                        if (has_hostapd || has_supplicant) {
-                                               encr.value('psk', 'WPA-PSK');
-                                               encr.value('psk2', 'WPA2-PSK');
-                                               encr.value('psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode');
+                                               crypto_modes.push(['psk2',      'WPA2-PSK',                    33]);
+                                               crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
+                                               crypto_modes.push(['psk',       'WPA-PSK',                     21]);
                                        }
                                        else {
                                                encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.');
                                        }
 
                                        if (has_ap_sae || has_sta_sae) {
-                                               encr.value('sae', 'WPA3-SAE');
-                                               encr.value('sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode');
+                                               crypto_modes.push(['sae',       'WPA3-SAE',                     31]);
+                                               crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]);
                                        }
 
                                        if (has_ap_eap || has_sta_eap) {
-                                               encr.value('wpa', 'WPA-EAP');
-                                               encr.value('wpa2', 'WPA2-EAP');
+                                               crypto_modes.push(['wpa2', 'WPA2-EAP', 32]);
+                                               crypto_modes.push(['wpa',  'WPA-EAP',  20]);
                                        }
 
                                        if (has_ap_owe || has_sta_owe) {
-                                               encr.value('owe', 'OWE');
+                                               crypto_modes.push(['owe', 'OWE', 1]);
                                        }
 
                                        encr.crypto_support = {
@@ -1154,9 +1187,23 @@ return L.view.extend({
                                        };
                                }
                                else if (hwtype == 'broadcom') {
-                                       encr.value('psk', 'WPA-PSK');
-                                       encr.value('psk2', 'WPA2-PSK');
-                                       encr.value('psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode');
+                                       crypto_modes.push(['psk2',     'WPA2-PSK',                    33]);
+                                       crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
+                                       crypto_modes.push(['psk',      'WPA-PSK',                     21]);
+                               }
+
+                               crypto_modes.push(['wep-open',   _('WEP Open System'), 11]);
+                               crypto_modes.push(['wep-shared', _('WEP Shared Key'),  10]);
+                               crypto_modes.push(['none',       _('No Encryption'),   0]);
+
+                               crypto_modes.sort(function(a, b) { return b[2] - a[2] });
+
+                               for (var i = 0; i < crypto_modes.length; i++) {
+                                       var security_level = (crypto_modes[i][2] >= 30) ? _('strong security')
+                                               : (crypto_modes[i][2] >= 20) ? _('medium security')
+                                                       : (crypto_modes[i][2] >= 10) ? _('weak security') : _('open network');
+
+                                       encr.value(crypto_modes[i][0], '%s (%s)'.format(crypto_modes[i][1], security_level));
                                }
 
 
@@ -1252,6 +1299,9 @@ return L.view.extend({
                                o.write = function(section_id, value) {
                                        uci.set('wireless', section_id, 'key', value);
                                        uci.unset('wireless', section_id, 'key1');
+                                       uci.unset('wireless', section_id, 'key2');
+                                       uci.unset('wireless', section_id, 'key3');
+                                       uci.unset('wireless', section_id, 'key4');
                                };
 
 
@@ -1265,7 +1315,7 @@ return L.view.extend({
 
                                o.cfgvalue = function(section_id) {
                                        var slot = +uci.get('wireless', section_id, 'key');
-                                       return String((slot >= 1 && slot <= 4) ? slot : 1);
+                                       return (slot >= 1 && slot <= 4) ? String(slot) : '';
                                };
 
                                o.write = function(section_id, value) {
@@ -1577,13 +1627,15 @@ return L.view.extend({
                                                o.depends({ mode: 'ap-wds', encryption: 'sae-mixed' });
 
                                                if (L.hasSystemFeature('hostapd', 'cli') && L.hasSystemFeature('wpasupplicant')) {
-                                                       o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK'))
+                                                       o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE'))
                                                        o.enabled = '1';
                                                        o.disabled = '0';
                                                        o.default = o.disabled;
                                                        o.depends('encryption', 'psk');
                                                        o.depends('encryption', 'psk2');
                                                        o.depends('encryption', 'psk-mixed');
+                                                       o.depends('encryption', 'sae');
+                                                       o.depends('encryption', 'sae-mixed');
                                                }
                                        }
                                }
@@ -1610,7 +1662,7 @@ return L.view.extend({
 
                        cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
 
-                       var md = L.ui.showModal(_('Join Network: Wireless Scan'), [
+                       var md = ui.showModal(_('Join Network: Wireless Scan'), [
                                table,
                                E('div', { 'class': 'right' },
                                        E('button', {
@@ -1690,7 +1742,7 @@ return L.view.extend({
                                md.style.maxHeight = '';
                        }
 
-                       L.ui.hideModal();
+                       ui.hideModal();
                        L.Poll.remove(this.pollFn);
 
                        this.pollFn = null;
@@ -1706,7 +1758,8 @@ return L.view.extend({
                            zoneval = zoneopt ? zoneopt.formvalue('_new_') : null,
                            enc = L.isObject(bss.encryption) ? bss.encryption : null,
                            is_wep = (enc && Array.isArray(enc.wep)),
-                           is_psk = (enc && Array.isArray(enc.wpa) && Array.isArray(enc.authentication) && enc.authentication[0] == 'psk');
+                           is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' })),
+                           is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }));
 
                        if (nameval == null || (passopt && passval == null))
                                return;
@@ -1714,15 +1767,23 @@ return L.view.extend({
                        var section_id = null;
 
                        return this.map.save(function() {
+                               var wifi_sections = uci.sections('wireless', 'wifi-iface');
+
                                if (replopt.formvalue('_new_') == '1') {
-                                       var sections = uci.sections('wireless', 'wifi-iface');
+                                       for (var i = 0; i < wifi_sections.length; i++)
+                                               if (wifi_sections[i].device == radioDev.getName())
+                                                       uci.remove('wireless', wifi_sections[i]['.name']);
+                               }
+
+                               if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') {
+                                       for (var i = 0; i < wifi_sections.length; i++)
+                                               if (wifi_sections[i].device == radioDev.getName())
+                                                       uci.set('wireless', wifi_sections[i]['.name'], 'disabled', '1');
 
-                                       for (var i = 0; i < sections.length; i++)
-                                               if (sections[i].device == radioDev.getName())
-                                                       uci.remove('wireless', sections[i]['.name']);
+                                       uci.unset('wireless', radioDev.getName(), 'disabled');
                                }
 
-                               section_id = next_free_sid(uci.sections('wifi-iface').length);
+                               section_id = next_free_sid(wifi_sections.length);
 
                                uci.add('wireless', 'wifi-iface', section_id);
                                uci.set('wireless', section_id, 'device', radioDev.getName());
@@ -1734,7 +1795,11 @@ return L.view.extend({
                                else if (bss.bssid != null)
                                        uci.set('wireless', section_id, 'bssid', bss.bssid);
 
-                               if (is_psk) {
+                               if (is_sae) {
+                                       uci.set('wireless', section_id, 'encryption', 'sae');
+                                       uci.set('wireless', section_id, 'key', passval);
+                               }
+                               else if (is_psk) {
                                        for (var i = enc.wpa.length - 1; i >= 0; i--) {
                                                if (enc.wpa[i] == 2) {
                                                        uci.set('wireless', section_id, 'encryption', 'psk2');
@@ -1754,14 +1819,14 @@ return L.view.extend({
                                        uci.set('wireless', section_id, 'key1', passval);
                                }
 
-                               var zonePromise = zoneval
-                                       ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) })
-                                       : Promise.resolve();
+                               return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) {
+                                       firewall.deleteNetwork(net.getName());
 
-                               return zonePromise.then(function(zone) {
-                                       return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) {
-                                               firewall.deleteNetwork(net.getName());
+                                       var zonePromise = zoneval
+                                               ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) })
+                                               : Promise.resolve();
 
+                                       return zonePromise.then(function(zone) {
                                                if (zone)
                                                        zone.addNetwork(net.getName());
                                        });
@@ -1778,7 +1843,7 @@ return L.view.extend({
                            s2 = m2.section(form.NamedSection, '_new_'),
                            enc = L.isObject(bss.encryption) ? bss.encryption : null,
                            is_wep = (enc && Array.isArray(enc.wep)),
-                           is_psk = (enc && Array.isArray(enc.wpa) && Array.isArray(enc.authentication) && enc.authentication[0] == 'psk'),
+                           is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae' })),
                            replace, passphrase, name, zone;
 
                        s2.render = function() {
@@ -1815,16 +1880,16 @@ return L.view.extend({
                        zone.default = 'wan';
 
                        return m2.render().then(L.bind(function(nodes) {
-                               L.ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [
+                               ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [
                                        nodes,
                                        E('div', { 'class': 'right' }, [
                                                E('button', {
                                                        'class': 'btn',
-                                                       'click': L.ui.hideModal
+                                                       'click': ui.hideModal
                                                }, _('Cancel')), ' ',
                                                E('button', {
                                                        'class': 'cbi-button cbi-button-positive important',
-                                                       'click': L.ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2)
+                                                       'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2)
                                                }, _('Submit'))
                                        ])
                                ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus();
@@ -1885,11 +1950,9 @@ return L.view.extend({
 
                                        if (dsc.getAttribute('restart') == '') {
                                                dsc.setAttribute('restart', '1');
-                                               tasks.push(L.Request.post(
-                                                       L.url('admin/network/wireless_reconnect', section_ids[i]),
-                                                       'token=' + L.env.token,
-                                                       { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
-                                               ).catch(function() {}));
+                                               tasks.push(fs.exec('/sbin/wifi', ['up', section_ids[i]]).catch(function(e) {
+                                                       ui.addNotification(null, E('p', e.message));
+                                               }));
                                        }
                                        else if (dsc.getAttribute('restart') == '1') {
                                                dsc.removeAttribute('restart');
@@ -1941,7 +2004,7 @@ return L.view.extend({
                                E('div', { 'class': 'tr table-titles' }, [
                                        E('div', { 'class': 'th nowrap' }, _('Network')),
                                        E('div', { 'class': 'th hide-xs' }, _('MAC-Address')),
-                                       E('div', { 'class': 'th nowrap' }, _('Host')),
+                                       E('div', { 'class': 'th' }, _('Host')),
                                        E('div', { 'class': 'th nowrap' }, _('Signal / Noise')),
                                        E('div', { 'class': 'th nowrap' }, _('RX Rate / TX Rate'))
                                ])