luci-base, luci-mod-network: adjust ZoneSelect / NetworkSelect descriptions
[oweals/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / wireless.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require fs';
6 'require ui';
7 'require rpc';
8 'require uci';
9 'require form';
10 'require network';
11 'require firewall';
12 'require tools.widgets as widgets';
13
14 function count_changes(section_id) {
15         var changes = ui.changes.changes, n = 0;
16
17         if (!L.isObject(changes))
18                 return n;
19
20         if (Array.isArray(changes.wireless))
21                 for (var i = 0; i < changes.wireless.length; i++)
22                         n += (changes.wireless[i][1] == section_id);
23
24         return n;
25 }
26
27 function render_radio_badge(radioDev) {
28         return E('span', { 'class': 'ifacebadge' }, [
29                 E('img', { 'src': L.resource('icons/wifi%s.png').format(radioDev.isUp() ? '' : '_disabled') }),
30                 ' ',
31                 radioDev.getName()
32         ]);
33 }
34
35 function render_signal_badge(signalPercent, signalValue, noiseValue, wrap, mode) {
36         var icon, title, value;
37
38         if (signalPercent < 0)
39                 icon = L.resource('icons/signal-none.png');
40         else if (signalPercent == 0)
41                 icon = L.resource('icons/signal-0.png');
42         else if (signalPercent < 25)
43                 icon = L.resource('icons/signal-0-25.png');
44         else if (signalPercent < 50)
45                 icon = L.resource('icons/signal-25-50.png');
46         else if (signalPercent < 75)
47                 icon = L.resource('icons/signal-50-75.png');
48         else
49                 icon = L.resource('icons/signal-75-100.png');
50
51         if (signalValue != null && signalValue != 0) {
52                 if (noiseValue != null && noiseValue != 0) {
53                         value = '%d/%d\xa0%s'.format(signalValue, noiseValue, _('dBm'));
54                         title = '%s: %d %s / %s: %d %s / %s %d'.format(
55                                 _('Signal'), signalValue, _('dBm'),
56                                 _('Noise'), noiseValue, _('dBm'),
57                                 _('SNR'), signalValue - noiseValue);
58                 }
59                 else {
60                         value = '%d\xa0%s'.format(signalValue, _('dBm'));
61                         title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm'));
62                 }
63         }
64         else if (signalPercent > -1) {
65                 switch (mode) {
66                         case 'ap':
67                                 title = _('No client associated');
68                                 break;
69
70                         case 'sta':
71                         case 'adhoc':
72                         case 'mesh':
73                                 title = _('Not associated');
74                                 break;
75
76                         default:
77                                 title = _('No RX signal');
78                 }
79
80                 if (noiseValue != null && noiseValue != 0) {
81                         value = '---/%d\x0a%s'.format(noiseValue, _('dBm'));
82                         title = '%s / %s: %d %s'.format(title, _('Noise'), noiseValue, _('dBm'));
83                 }
84                 else {
85                         value = '---\xa0%s'.format(_('dBm'));
86                 }
87         }
88         else {
89                 value = E('em', {}, E('small', {}, [ _('disabled') ]));
90                 title = _('Interface is disabled');
91         }
92
93         return E('div', {
94                 'class': wrap ? 'center' : 'ifacebadge',
95                 'title': title,
96                 'data-signal': signalValue,
97                 'data-noise': noiseValue
98         }, [
99                 E('img', { 'src': icon }),
100                 E('span', {}, [
101                         wrap ? E('br') : ' ',
102                         value
103                 ])
104         ]);
105 }
106
107 function render_network_badge(radioNet) {
108         return render_signal_badge(
109                 radioNet.isUp() ? radioNet.getSignalPercent() : -1,
110                 radioNet.getSignal(), radioNet.getNoise(), false, radioNet.getMode());
111 }
112
113 function render_radio_status(radioDev, wifiNets) {
114         var name = radioDev.getI18n().replace(/ Wireless Controller .+$/, ''),
115             node = E('div', [ E('big', {}, E('strong', {}, name)), E('div') ]),
116             channel, frequency, bitrate;
117
118         for (var i = 0; i < wifiNets.length; i++) {
119                 channel   = channel   || wifiNets[i].getChannel();
120                 frequency = frequency || wifiNets[i].getFrequency();
121                 bitrate   = bitrate   || wifiNets[i].getBitRate();
122         }
123
124         if (radioDev.isUp())
125                 L.itemlist(node.lastElementChild, [
126                         _('Channel'), '%s (%s %s)'.format(channel || '?', frequency || '?', _('GHz')),
127                         _('Bitrate'), '%s %s'.format(bitrate || '?', _('Mbit/s'))
128                 ], ' | ');
129         else
130                 node.lastElementChild.appendChild(E('em', _('Device is not active')));
131
132         return node;
133 }
134
135 function render_network_status(radioNet) {
136         var mode = radioNet.getActiveMode(),
137             bssid = radioNet.getActiveBSSID(),
138             channel = radioNet.getChannel(),
139             disabled = (radioNet.get('disabled') == '1' || uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == '1'),
140             is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled),
141             is_mesh = (radioNet.getMode() == 'mesh'),
142             changecount = count_changes(radioNet.getName()),
143             status_text = null;
144
145         if (changecount)
146                 status_text = E('a', {
147                         href: '#',
148                         click: L.bind(ui.changes.displayChanges, ui.changes)
149                 }, _('Interface has %d pending changes').format(changecount));
150         else if (!is_assoc)
151                 status_text = E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated'));
152
153         return L.itemlist(E('div'), [
154                 is_mesh ? _('Mesh ID') : _('SSID'), (is_mesh ? radioNet.getMeshID() : radioNet.getSSID()) || '?',
155                 _('Mode'),       mode,
156                 _('BSSID'),      (!changecount && is_assoc) ? bssid : null,
157                 _('Encryption'), (!changecount && is_assoc) ? radioNet.getActiveEncryption() || _('None') : null,
158                 null,            status_text
159         ], [ ' | ', E('br') ]);
160 }
161
162 function render_modal_status(node, radioNet) {
163         var mode = radioNet.getActiveMode(),
164             noise = radioNet.getNoise(),
165             bssid = radioNet.getActiveBSSID(),
166             channel = radioNet.getChannel(),
167             disabled = (radioNet.get('disabled') == '1'),
168             is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled);
169
170         if (node == null)
171                 node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getName() }, [ E('small'), E('span') ]);
172
173         dom.content(node.firstElementChild, render_signal_badge(
174                 disabled ? -1 : radioNet.getSignalPercent(),
175                 radioNet.getSignal(), noise, true, radioNet.getMode()));
176
177         L.itemlist(node.lastElementChild, [
178                 _('Mode'),       mode,
179                 _('SSID'),       radioNet.getSSID() || '?',
180                 _('BSSID'),      is_assoc ? bssid : null,
181                 _('Encryption'), is_assoc ? radioNet.getActiveEncryption() || _('None') : null,
182                 _('Channel'),    is_assoc ? '%d (%.3f %s)'.format(radioNet.getChannel(), radioNet.getFrequency() || 0, _('GHz')) : null,
183                 _('Tx-Power'),   is_assoc ? '%d %s'.format(radioNet.getTXPower(), _('dBm')) : null,
184                 _('Signal'),     is_assoc ? '%d %s'.format(radioNet.getSignal(), _('dBm')) : null,
185                 _('Noise'),      (is_assoc && noise != null) ? '%d %s'.format(noise, _('dBm')) : null,
186                 _('Bitrate'),    is_assoc ? '%.1f %s'.format(radioNet.getBitRate() || 0, _('Mbit/s')) : null,
187                 _('Country'),    is_assoc ? radioNet.getCountryCode() : null
188         ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]);
189
190         if (!is_assoc)
191                 dom.append(node.lastElementChild, E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')));
192
193         return node;
194 }
195
196 function format_wifirate(rate) {
197         var s = '%.1f\xa0%s, %d\xa0%s'.format(rate.rate / 1000, _('Mbit/s'), rate.mhz, _('MHz')),
198             ht = rate.ht, vht = rate.vht,
199             mhz = rate.mhz, nss = rate.nss,
200             mcs = rate.mcs, sgi = rate.short_gi;
201
202         if (ht || vht) {
203                 if (vht) s += ', VHT-MCS\xa0%d'.format(mcs);
204                 if (nss) s += ', VHT-NSS\xa0%d'.format(nss);
205                 if (ht)  s += ', MCS\xa0%s'.format(mcs);
206                 if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0');
207         }
208
209         return s;
210 }
211
212 function radio_restart(id, ev) {
213         var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)),
214             dsc = row.querySelector('[data-name="_stat"] > div'),
215             btn = row.querySelector('.cbi-section-actions button');
216
217         btn.blur();
218         btn.classList.add('spinning');
219         btn.disabled = true;
220
221         dsc.setAttribute('restart', '');
222         dom.content(dsc, E('em', _('Device is restarting…')));
223 }
224
225 function network_updown(id, map, ev) {
226         var radio = uci.get('wireless', id, 'device'),
227             disabled = (uci.get('wireless', id, 'disabled') == '1') ||
228                        (uci.get('wireless', radio, 'disabled') == '1');
229
230         if (disabled) {
231                 uci.unset('wireless', id, 'disabled');
232                 uci.unset('wireless', radio, 'disabled');
233         }
234         else {
235                 uci.set('wireless', id, 'disabled', '1');
236
237                 var all_networks_disabled = true,
238                     wifi_ifaces = uci.sections('wireless', 'wifi-iface');
239
240                 for (var i = 0; i < wifi_ifaces.length; i++) {
241                         if (wifi_ifaces[i].device == radio && wifi_ifaces[i].disabled != '1') {
242                                 all_networks_disabled = false;
243                                 break;
244                         }
245                 }
246
247                 if (all_networks_disabled)
248                         uci.set('wireless', radio, 'disabled', '1');
249         }
250
251         return map.save().then(function() {
252                 ui.changes.apply()
253         });
254 }
255
256 function next_free_sid(offset) {
257         var sid = 'wifinet' + offset;
258
259         while (uci.get('wireless', sid))
260                 sid = 'wifinet' + (++offset);
261
262         return sid;
263 }
264
265 function add_dependency_permutations(o, deps) {
266         var res = null;
267
268         for (var key in deps) {
269                 if (!deps.hasOwnProperty(key) || !Array.isArray(deps[key]))
270                         continue;
271
272                 var list = deps[key],
273                     tmp = [];
274
275                 for (var j = 0; j < list.length; j++) {
276                         for (var k = 0; k < (res ? res.length : 1); k++) {
277                                 var item = (res ? Object.assign({}, res[k]) : {});
278                                 item[key] = list[j];
279                                 tmp.push(item);
280                         }
281                 }
282
283                 res = tmp;
284         }
285
286         for (var i = 0; i < (res ? res.length : 0); i++)
287                 o.depends(res[i]);
288 }
289
290 var CBIWifiFrequencyValue = form.Value.extend({
291         callFrequencyList: rpc.declare({
292                 object: 'iwinfo',
293                 method: 'freqlist',
294                 params: [ 'device' ],
295                 expect: { results: [] }
296         }),
297
298         load: function(section_id) {
299                 return Promise.all([
300                         network.getWifiDevice(section_id),
301                         this.callFrequencyList(section_id)
302                 ]).then(L.bind(function(data) {
303                         this.channels = {
304                                 '11g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
305                                 '11a': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : []
306                         };
307
308                         for (var i = 0; i < data[1].length; i++)
309                                 this.channels[(data[1][i].mhz > 2484) ? '11a' : '11g'].push(
310                                         data[1][i].channel,
311                                         '%d (%d Mhz)'.format(data[1][i].channel, data[1][i].mhz),
312                                         !data[1][i].restricted
313                                 );
314
315                         var hwmodelist = L.toArray(data[0] ? data[0].getHWModes() : null)
316                                 .reduce(function(o, v) { o[v] = true; return o }, {});
317
318                         this.modes = [
319                                 '', 'Legacy', true,
320                                 'n', 'N', hwmodelist.n,
321                                 'ac', 'AC', hwmodelist.ac
322                         ];
323
324                         var htmodelist = L.toArray(data[0] ? data[0].getHTModes() : null)
325                                 .reduce(function(o, v) { o[v] = true; return o }, {});
326
327                         this.htmodes = {
328                                 '': [ '', '-', true ],
329                                 'n': [
330                                         'HT20', '20 MHz', htmodelist.HT20,
331                                         'HT40', '40 MHz', htmodelist.HT40
332                                 ],
333                                 'ac': [
334                                         'VHT20', '20 MHz', htmodelist.VHT20,
335                                         'VHT40', '40 MHz', htmodelist.VHT40,
336                                         'VHT80', '80 MHz', htmodelist.VHT80,
337                                         'VHT160', '160 MHz', htmodelist.VHT160
338                                 ]
339                         };
340
341                         this.bands = {
342                                 '': [
343                                         '11g', '2.4 GHz', this.channels['11g'].length > 3,
344                                         '11a', '5 GHz', this.channels['11a'].length > 3
345                                 ],
346                                 'n': [
347                                         '11g', '2.4 GHz', this.channels['11g'].length > 3,
348                                         '11a', '5 GHz', this.channels['11a'].length > 3
349                                 ],
350                                 'ac': [
351                                         '11a', '5 GHz', true
352                                 ]
353                         };
354                 }, this));
355         },
356
357         setValues: function(sel, vals) {
358                 if (sel.vals)
359                         sel.vals.selected = sel.selectedIndex;
360
361                 while (sel.options[0])
362                         sel.remove(0);
363
364                 for (var i = 0; vals && i < vals.length; i += 3)
365                         if (vals[i+2])
366                                 sel.add(E('option', { value: vals[i+0] }, [ vals[i+1] ]));
367
368                 if (vals && !isNaN(vals.selected))
369                         sel.selectedIndex = vals.selected;
370
371                 sel.parentNode.style.display = (sel.options.length <= 1) ? 'none' : '';
372                 sel.vals = vals;
373         },
374
375         toggleWifiMode: function(elem) {
376                 this.toggleWifiHTMode(elem);
377                 this.toggleWifiBand(elem);
378         },
379
380         toggleWifiHTMode: function(elem) {
381                 var mode = elem.querySelector('.mode');
382                 var bwdt = elem.querySelector('.htmode');
383
384                 this.setValues(bwdt, this.htmodes[mode.value]);
385         },
386
387         toggleWifiBand: function(elem) {
388                 var mode = elem.querySelector('.mode');
389                 var band = elem.querySelector('.band');
390
391                 this.setValues(band, this.bands[mode.value]);
392                 this.toggleWifiChannel(elem);
393         },
394
395         toggleWifiChannel: function(elem) {
396                 var band = elem.querySelector('.band');
397                 var chan = elem.querySelector('.channel');
398
399                 this.setValues(chan, this.channels[band.value]);
400         },
401
402         setInitialValues: function(section_id, elem) {
403                 var mode = elem.querySelector('.mode'),
404                     band = elem.querySelector('.band'),
405                     chan = elem.querySelector('.channel'),
406                     bwdt = elem.querySelector('.htmode'),
407                     htval = uci.get('wireless', section_id, 'htmode'),
408                     hwval = uci.get('wireless', section_id, 'hwmode'),
409                     chval = uci.get('wireless', section_id, 'channel');
410
411                 this.setValues(mode, this.modes);
412
413                 if (/VHT20|VHT40|VHT80|VHT160/.test(htval))
414                         mode.value = 'ac';
415                 else if (/HT20|HT40/.test(htval))
416                         mode.value = 'n';
417                 else
418                         mode.value = '';
419
420                 this.toggleWifiMode(elem);
421
422                 if (/a/.test(hwval))
423                         band.value = '11a';
424                 else
425                         band.value = '11g';
426
427                 this.toggleWifiBand(elem);
428
429                 bwdt.value = htval;
430                 chan.value = chval;
431
432                 return elem;
433         },
434
435         renderWidget: function(section_id, option_index, cfgvalue) {
436                 var elem = E('div');
437
438                 dom.content(elem, [
439                         E('label', { 'style': 'float:left; margin-right:3px' }, [
440                                 _('Mode'), E('br'),
441                                 E('select', {
442                                         'class': 'mode',
443                                         'style': 'width:auto',
444                                         'change': L.bind(this.toggleWifiMode, this, elem)
445                                 })
446                         ]),
447                         E('label', { 'style': 'float:left; margin-right:3px' }, [
448                                 _('Band'), E('br'),
449                                 E('select', {
450                                         'class': 'band',
451                                         'style': 'width:auto',
452                                         'change': L.bind(this.toggleWifiBand, this, elem)
453                                 })
454                         ]),
455                         E('label', { 'style': 'float:left; margin-right:3px' }, [
456                                 _('Channel'), E('br'),
457                                 E('select', {
458                                         'class': 'channel',
459                                         'style': 'width:auto'
460                                 })
461                         ]),
462                         E('label', { 'style': 'float:left; margin-right:3px' }, [
463                                 _('Width'), E('br'),
464                                 E('select', {
465                                         'class': 'htmode',
466                                         'style': 'width:auto'
467                                 })
468                         ]),
469                         E('br', { 'style': 'clear:left' })
470                 ]);
471
472                 return this.setInitialValues(section_id, elem);
473         },
474
475         cfgvalue: function(section_id) {
476                 return [
477                     uci.get('wireless', section_id, 'htmode'),
478                     uci.get('wireless', section_id, 'hwmode'),
479                     uci.get('wireless', section_id, 'channel')
480                 ];
481         },
482
483         formvalue: function(section_id) {
484                 var node = this.map.findElement('data-field', this.cbid(section_id));
485
486                 return [
487                     node.querySelector('.htmode').value,
488                     node.querySelector('.band').value,
489                         node.querySelector('.channel').value
490                 ];
491         },
492
493         write: function(section_id, value) {
494                 uci.set('wireless', section_id, 'htmode', value[0] || null);
495                 uci.set('wireless', section_id, 'hwmode', value[1]);
496                 uci.set('wireless', section_id, 'channel', value[2]);
497         }
498 });
499
500 var CBIWifiTxPowerValue = form.ListValue.extend({
501         callTxPowerList: rpc.declare({
502                 object: 'iwinfo',
503                 method: 'txpowerlist',
504                 params: [ 'device' ],
505                 expect: { results: [] }
506         }),
507
508         load: function(section_id) {
509                 return this.callTxPowerList(section_id).then(L.bind(function(pwrlist) {
510                         this.powerval = this.wifiNetwork ? this.wifiNetwork.getTXPower() : null;
511                         this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
512
513                         this.value('', _('driver default'));
514
515                         for (var i = 0; i < pwrlist.length; i++)
516                                 this.value(pwrlist[i].dbm, '%d dBm (%d mW)'.format(pwrlist[i].dbm, pwrlist[i].mw));
517
518                         return form.ListValue.prototype.load.apply(this, [section_id]);
519                 }, this));
520         },
521
522         renderWidget: function(section_id, option_index, cfgvalue) {
523                 var widget = form.ListValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
524                     widget.firstElementChild.style.width = 'auto';
525
526                 dom.append(widget, E('span', [
527                         ' - ', _('Current power'), ': ',
528                         E('span', [ this.powerval != null ? '%d dBm'.format(this.powerval) : E('em', _('unknown')) ]),
529                         this.poweroff ? ' + %d dB offset = %s dBm'.format(this.poweroff, this.powerval != null ? this.powerval + this.poweroff : '?') : ''
530                 ]));
531
532                 return widget;
533         }
534 });
535
536 var CBIWifiCountryValue = form.Value.extend({
537         callCountryList: rpc.declare({
538                 object: 'iwinfo',
539                 method: 'countrylist',
540                 params: [ 'device' ],
541                 expect: { results: [] }
542         }),
543
544         load: function(section_id) {
545                 return this.callCountryList(section_id).then(L.bind(function(countrylist) {
546                         if (Array.isArray(countrylist) && countrylist.length > 0) {
547                                 this.value('', _('driver default'));
548
549                                 for (var i = 0; i < countrylist.length; i++)
550                                         this.value(countrylist[i].iso3166, '%s - %s'.format(countrylist[i].iso3166, countrylist[i].country));
551                         }
552
553                         return form.Value.prototype.load.apply(this, [section_id]);
554                 }, this));
555         },
556
557         validate: function(section_id, formvalue) {
558                 if (formvalue != null && formvalue != '' && !/^[A-Z0-9][A-Z0-9]$/.test(formvalue))
559                         return _('Use ISO/IEC 3166 alpha2 country codes.');
560
561                 return true;
562         },
563
564         renderWidget: function(section_id, option_index, cfgvalue) {
565                 var typeClass = (this.keylist && this.keylist.length) ? form.ListValue : form.Value;
566                 return typeClass.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
567         }
568 });
569
570 return view.extend({
571         poll_status: function(map, data) {
572                 var rows = map.querySelectorAll('.cbi-section-table-row[data-sid]');
573
574                 for (var i = 0; i < rows.length; i++) {
575                         var section_id = rows[i].getAttribute('data-sid'),
576                             radioDev = data[1].filter(function(d) { return d.getName() == section_id })[0],
577                             radioNet = data[2].filter(function(n) { return n.getName() == section_id })[0],
578                             badge = rows[i].querySelector('[data-name="_badge"] > div'),
579                             stat = rows[i].querySelector('[data-name="_stat"]'),
580                             btns = rows[i].querySelectorAll('.cbi-section-actions button'),
581                             busy = btns[0].classList.contains('spinning') || btns[1].classList.contains('spinning') || btns[2].classList.contains('spinning');
582
583                         if (radioDev) {
584                                 dom.content(badge, render_radio_badge(radioDev));
585                                 dom.content(stat, render_radio_status(radioDev, data[2].filter(function(n) { return n.getWifiDeviceName() == radioDev.getName() })));
586                         }
587                         else {
588                                 dom.content(badge, render_network_badge(radioNet));
589                                 dom.content(stat, render_network_status(radioNet));
590                         }
591
592                         if (stat.hasAttribute('restart'))
593                                 dom.content(stat, E('em', _('Device is restarting…')));
594
595                         btns[0].disabled = busy;
596                         btns[1].disabled = busy;
597                         btns[2].disabled = busy;
598                 }
599
600                 var table = document.querySelector('#wifi_assoclist_table'),
601                     hosts = data[0],
602                     trows = [];
603
604                 for (var i = 0; i < data[3].length; i++) {
605                         var bss = data[3][i],
606                             name = hosts.getHostnameByMACAddr(bss.mac),
607                             ipv4 = hosts.getIPAddrByMACAddr(bss.mac),
608                             ipv6 = hosts.getIP6AddrByMACAddr(bss.mac);
609
610                         var hint;
611
612                         if (name && ipv4 && ipv6)
613                                 hint = '%s <span class="hide-xs">(%s, %s)</span>'.format(name, ipv4, ipv6);
614                         else if (name && (ipv4 || ipv6))
615                                 hint = '%s <span class="hide-xs">(%s)</span>'.format(name, ipv4 || ipv6);
616                         else
617                                 hint = name || ipv4 || ipv6 || '?';
618
619                         var row = [
620                                 E('span', {
621                                         'class': 'ifacebadge',
622                                         'data-ifname': bss.network.getIfname(),
623                                         'data-ssid': bss.network.getSSID()
624                                 }, [
625                                         E('img', {
626                                                 'src': L.resource('icons/wifi%s.png').format(bss.network.isUp() ? '' : '_disabled'),
627                                                 'title': bss.radio.getI18n()
628                                         }),
629                                         E('span', [
630                                                 ' %s '.format(bss.network.getShortName()),
631                                                 E('small', '(%s)'.format(bss.network.getIfname()))
632                                         ])
633                                 ]),
634                                 bss.mac,
635                                 hint,
636                                 render_signal_badge(Math.min((bss.signal + 110) / 70 * 100, 100), bss.signal, bss.noise),
637                                 E('span', {}, [
638                                         E('span', format_wifirate(bss.rx)),
639                                         E('br'),
640                                         E('span', format_wifirate(bss.tx))
641                                 ])
642                         ];
643
644                         if (bss.network.isClientDisconnectSupported()) {
645                                 if (table.firstElementChild.childNodes.length < 6)
646                                         table.firstElementChild.appendChild(E('div', { 'class': 'th cbi-section-actions'}));
647
648                                 row.push(E('button', {
649                                         'class': 'cbi-button cbi-button-remove',
650                                         'click': L.bind(function(net, mac, ev) {
651                                                 dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
652                                                 ev.currentTarget.classList.add('spinning');
653                                                 ev.currentTarget.disabled = true;
654                                                 ev.currentTarget.blur();
655
656                                                 net.disconnectClient(mac, true, 5, 60000);
657                                         }, this, bss.network, bss.mac)
658                                 }, [ _('Disconnect') ]));
659                         }
660                         else {
661                                 row.push('-');
662                         }
663
664                         trows.push(row);
665                 }
666
667                 cbi_update_table(table, trows, E('em', _('No information available')));
668
669                 var stat = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large');
670
671                 if (stat)
672                         render_modal_status(stat, data[2].filter(function(n) { return n.getName() == stat.getAttribute('data-network') })[0]);
673
674                 return network.flushCache();
675         },
676
677         load: function() {
678                 return Promise.all([
679                         uci.changes(),
680                         uci.load('wireless')
681                 ]);
682         },
683
684         checkAnonymousSections: function() {
685                 var wifiIfaces = uci.sections('wireless', 'wifi-iface');
686
687                 for (var i = 0; i < wifiIfaces.length; i++)
688                         if (wifiIfaces[i]['.anonymous'])
689                                 return true;
690
691                 return false;
692         },
693
694         callUciRename: rpc.declare({
695                 object: 'uci',
696                 method: 'rename',
697                 params: [ 'config', 'section', 'name' ]
698         }),
699
700         render: function() {
701                 if (this.checkAnonymousSections())
702                         return this.renderMigration();
703                 else
704                         return this.renderOverview();
705         },
706
707         handleMigration: function(ev) {
708                 var wifiIfaces = uci.sections('wireless', 'wifi-iface'),
709                     id_offset = 0,
710                     tasks = [];
711
712                 for (var i = 0; i < wifiIfaces.length; i++) {
713                         if (!wifiIfaces[i]['.anonymous'])
714                                 continue;
715
716                         var new_name = next_free_sid(id_offset);
717
718                         tasks.push(this.callUciRename('wireless', wifiIfaces[i]['.name'], new_name));
719                         id_offset = +new_name.substring(7) + 1;
720                 }
721
722                 return Promise.all(tasks)
723                         .then(L.bind(ui.changes.init, ui.changes))
724                         .then(L.bind(ui.changes.apply, ui.changes));
725         },
726
727         renderMigration: function() {
728                 ui.showModal(_('Wireless configuration migration'), [
729                         E('p', _('The existing wireless configuration needs to be changed for LuCI to function properly.')),
730                         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.')),
731                         E('div', { 'class': 'right' },
732                                 E('button', {
733                                         'class': 'btn cbi-button-action important',
734                                         'click': ui.createHandlerFn(this, 'handleMigration')
735                                 }, _('Continue')))
736                 ]);
737         },
738
739         renderOverview: function() {
740                 var m, s, o;
741
742                 m = new form.Map('wireless');
743                 m.chain('network');
744                 m.chain('firewall');
745
746                 s = m.section(form.GridSection, 'wifi-device', _('Wireless Overview'));
747                 s.anonymous = true;
748                 s.addremove = false;
749
750                 s.load = function() {
751                         return network.getWifiDevices().then(L.bind(function(radios) {
752                                 this.radios = radios.sort(function(a, b) {
753                                         return a.getName() > b.getName();
754                                 });
755
756                                 var tasks = [];
757
758                                 for (var i = 0; i < radios.length; i++)
759                                         tasks.push(radios[i].getWifiNetworks());
760
761                                 return Promise.all(tasks);
762                         }, this)).then(L.bind(function(data) {
763                                 this.wifis = [];
764
765                                 for (var i = 0; i < data.length; i++)
766                                         this.wifis.push.apply(this.wifis, data[i]);
767                         }, this));
768                 };
769
770                 s.cfgsections = function() {
771                         var rv = [];
772
773                         for (var i = 0; i < this.radios.length; i++) {
774                                 rv.push(this.radios[i].getName());
775
776                                 for (var j = 0; j < this.wifis.length; j++)
777                                         if (this.wifis[j].getWifiDeviceName() == this.radios[i].getName())
778                                                 rv.push(this.wifis[j].getName());
779                         }
780
781                         return rv;
782                 };
783
784                 s.modaltitle = function(section_id) {
785                         var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id})[0];
786                         return radioNet ? radioNet.getI18n() : _('Edit wireless network');
787                 };
788
789                 s.lookupRadioOrNetwork = function(section_id) {
790                         var radioDev = this.radios.filter(function(r) { return r.getName() == section_id })[0];
791                         if (radioDev)
792                                 return radioDev;
793
794                         var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id })[0];
795                         if (radioNet)
796                                 return radioNet;
797
798                         return null;
799                 };
800
801                 s.renderRowActions = function(section_id) {
802                         var inst = this.lookupRadioOrNetwork(section_id), btns;
803
804                         if (inst.getWifiNetworks) {
805                                 btns = [
806                                         E('button', {
807                                                 'class': 'cbi-button cbi-button-neutral',
808                                                 'title': _('Restart radio interface'),
809                                                 'click': ui.createHandlerFn(this, radio_restart, section_id)
810                                         }, _('Restart')),
811                                         E('button', {
812                                                 'class': 'cbi-button cbi-button-action important',
813                                                 'title': _('Find and join network'),
814                                                 'click': ui.createHandlerFn(this, 'handleScan', inst)
815                                         }, _('Scan')),
816                                         E('button', {
817                                                 'class': 'cbi-button cbi-button-add',
818                                                 'title': _('Provide new network'),
819                                                 'click': ui.createHandlerFn(this, 'handleAdd', inst)
820                                         }, _('Add'))
821                                 ];
822                         }
823                         else {
824                                 var isDisabled = (inst.get('disabled') == '1' ||
825                                         uci.get('wireless', inst.getWifiDeviceName(), 'disabled') == '1');
826
827                                 btns = [
828                                         E('button', {
829                                                 'class': 'cbi-button cbi-button-neutral enable-disable',
830                                                 'title': isDisabled ? _('Enable this network') : _('Disable this network'),
831                                                 'click': ui.createHandlerFn(this, network_updown, section_id, this.map)
832                                         }, isDisabled ? _('Enable') : _('Disable')),
833                                         E('button', {
834                                                 'class': 'cbi-button cbi-button-action important',
835                                                 'title': _('Edit this network'),
836                                                 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
837                                         }, _('Edit')),
838                                         E('button', {
839                                                 'class': 'cbi-button cbi-button-negative remove',
840                                                 'title': _('Delete this network'),
841                                                 'click': ui.createHandlerFn(this, 'handleRemove', section_id)
842                                         }, _('Remove'))
843                                 ];
844                         }
845
846                         return E('div', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
847                 };
848
849                 s.addModalOptions = function(s) {
850                         return network.getWifiNetwork(s.section).then(function(radioNet) {
851                                 var hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type');
852                                 var o, ss;
853
854                                 o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getWifiDeviceName(), 'wifi-device', _('Device Configuration'));
855                                 o.modalonly = true;
856
857                                 ss = o.subsection;
858                                 ss.tab('general', _('General Setup'));
859                                 ss.tab('advanced', _('Advanced Settings'));
860
861                                 var isDisabled = (radioNet.get('disabled') == '1' ||
862                                         uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == 1);
863
864                                 o = ss.taboption('general', form.DummyValue, '_wifistat_modal', _('Status'));
865                                 o.cfgvalue = L.bind(function(radioNet) {
866                                         return render_modal_status(null, radioNet);
867                                 }, this, radioNet);
868                                 o.write = function() {};
869
870                                 o = ss.taboption('general', form.Button, '_toggle', isDisabled ? _('Wireless network is disabled') : _('Wireless network is enabled'));
871                                 o.inputstyle = isDisabled ? 'apply' : 'reset';
872                                 o.inputtitle = isDisabled ? _('Enable') : _('Disable');
873                                 o.onclick = ui.createHandlerFn(s, network_updown, s.section, s.map);
874
875                                 o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '<br />' + _('Operating frequency'));
876                                 o.ucisection = s.section;
877
878                                 if (hwtype == 'mac80211') {
879                                         o = ss.taboption('general', CBIWifiTxPowerValue, 'txpower', _('Maximum transmit power'), _('Specifies the maximum transmit power the wireless radio may use. Depending on regulatory requirements and wireless usage, the actual transmit power may be reduced by the driver.'));
880                                         o.wifiNetwork = radioNet;
881
882                                         o = ss.taboption('advanced', CBIWifiCountryValue, 'country', _('Country Code'));
883                                         o.wifiNetwork = radioNet;
884
885                                         o = ss.taboption('advanced', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'));
886                                         o.default = o.enabled;
887
888                                         o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters.'));
889                                         o.datatype = 'range(0,114750)';
890                                         o.placeholder = 'auto';
891
892                                         o = ss.taboption('advanced', form.Value, 'frag', _('Fragmentation Threshold'));
893                                         o.datatype = 'min(256)';
894                                         o.placeholder = _('off');
895
896                                         o = ss.taboption('advanced', form.Value, 'rts', _('RTS/CTS Threshold'));
897                                         o.datatype = 'uinteger';
898                                         o.placeholder = _('off');
899
900                                         o = ss.taboption('advanced', form.Flag, 'noscan', _('Force 40MHz mode'), _('Always use 40MHz channels even if the secondary channel overlaps. Using this option does not comply with IEEE 802.11n-2009!'));
901                                         o.rmempty = true;
902
903                                         o = ss.taboption('advanced', form.Value, 'beacon_int', _('Beacon Interval'));
904                                         o.datatype = 'range(15,65535)';
905                                         o.placeholder = 100;
906                                         o.rmempty = true;
907                                 }
908
909
910                                 o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getName(), 'wifi-iface', _('Interface Configuration'));
911                                 o.modalonly = true;
912
913                                 ss = o.subsection;
914                                 ss.tab('general', _('General Setup'));
915                                 ss.tab('encryption', _('Wireless Security'));
916                                 ss.tab('macfilter', _('MAC-Filter'));
917                                 ss.tab('advanced', _('Advanced Settings'));
918
919                                 o = ss.taboption('general', form.ListValue, 'mode', _('Mode'));
920                                 o.value('ap', _('Access Point'));
921                                 o.value('sta', _('Client'));
922                                 o.value('adhoc', _('Ad-Hoc'));
923
924                                 o = ss.taboption('general', form.Value, 'mesh_id', _('Mesh Id'));
925                                 o.depends('mode', 'mesh');
926
927                                 o = ss.taboption('advanced', form.Flag, 'mesh_fwding', _('Forward mesh peer traffic'));
928                                 o.rmempty = false;
929                                 o.default = '1';
930                                 o.depends('mode', 'mesh');
931
932                                 o = ss.taboption('advanced', form.Value, 'mesh_rssi_threshold', _('RSSI threshold for joining'), _('0 = not using RSSI threshold, 1 = do not change driver default'));
933                                 o.rmempty = false;
934                                 o.default = '0';
935                                 o.datatype = 'range(-255,1)';
936                                 o.depends('mode', 'mesh');
937
938                                 o = ss.taboption('general', form.Value, 'ssid', _('<abbr title="Extended Service Set Identifier">ESSID</abbr>'));
939                                 o.datatype = 'maxlength(32)';
940                                 o.depends('mode', 'ap');
941                                 o.depends('mode', 'sta');
942                                 o.depends('mode', 'adhoc');
943                                 o.depends('mode', 'ahdemo');
944                                 o.depends('mode', 'monitor');
945                                 o.depends('mode', 'ap-wds');
946                                 o.depends('mode', 'sta-wds');
947                                 o.depends('mode', 'wds');
948
949                                 o = ss.taboption('general', form.Value, 'bssid', _('<abbr title="Basic Service Set Identifier">BSSID</abbr>'));
950                                 o.datatype = 'macaddr';
951
952                                 o = ss.taboption('general', widgets.NetworkSelect, 'network', _('Network'), _('Choose the network(s) you want to attach to this wireless interface or fill out the <em>custom</em> field to define a new network.'));
953                                 o.rmempty = true;
954                                 o.multiple = true;
955                                 o.novirtual = true;
956                                 o.write = function(section_id, value) {
957                                         return network.getDevice(section_id).then(L.bind(function(dev) {
958                                                 var old_networks = dev.getNetworks().reduce(function(o, v) { o[v.getName()] = v; return o }, {}),
959                                                     new_networks = {},
960                                                     values = L.toArray(value),
961                                                     tasks = [];
962
963                                                 for (var i = 0; i < values.length; i++) {
964                                                         new_networks[values[i]] = true;
965
966                                                         if (old_networks[values[i]])
967                                                                 continue;
968
969                                                         tasks.push(network.getNetwork(values[i]).then(L.bind(function(name, net) {
970                                                                 return net || network.addNetwork(name, { proto: 'none' });
971                                                         }, this, values[i])).then(L.bind(function(dev, net) {
972                                                                 if (net) {
973                                                                         if (!net.isEmpty())
974                                                                                 net.set('type', 'bridge');
975                                                                         net.addDevice(dev);
976                                                                 }
977                                                         }, this, dev)));
978                                                 }
979
980                                                 for (var name in old_networks)
981                                                         if (!new_networks[name])
982                                                                 tasks.push(network.getNetwork(name).then(L.bind(function(dev, net) {
983                                                                         if (net)
984                                                                                 net.deleteDevice(dev);
985                                                                 }, this, dev)));
986
987                                                 return Promise.all(tasks);
988                                         }, this));
989                                 };
990
991                                 if (hwtype == 'mac80211') {
992                                         var mode = ss.children[0],
993                                             bssid = ss.children[5],
994                                             encr;
995
996                                         mode.value('mesh', '802.11s');
997                                         mode.value('ahdemo', _('Pseudo Ad-Hoc (ahdemo)'));
998                                         mode.value('monitor', _('Monitor'));
999
1000                                         bssid.depends('mode', 'adhoc');
1001                                         bssid.depends('mode', 'sta');
1002                                         bssid.depends('mode', 'sta-wds');
1003
1004                                         o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC-Address Filter'));
1005                                         o.depends('mode', 'ap');
1006                                         o.depends('mode', 'ap-wds');
1007                                         o.value('', _('disable'));
1008                                         o.value('allow', _('Allow listed only'));
1009                                         o.value('deny', _('Allow all except listed'));
1010
1011                                         o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List'));
1012                                         o.datatype = 'macaddr';
1013                                         o.depends('macfilter', 'allow');
1014                                         o.depends('macfilter', 'deny');
1015                                         o.load = function(section_id) {
1016                                                 return network.getHostHints().then(L.bind(function(hints) {
1017                                                         hints.getMACHints().map(L.bind(function(hint) {
1018                                                                 this.value(hint[0], hint[1] ? '%s (%s)'.format(hint[0], hint[1]) : hint[0]);
1019                                                         }, this));
1020
1021                                                         return form.DynamicList.prototype.load.apply(this, [section_id]);
1022                                                 }, this));
1023                                         };
1024
1025                                         mode.value('ap-wds', '%s (%s)'.format(_('Access Point'), _('WDS')));
1026                                         mode.value('sta-wds', '%s (%s)'.format(_('Client'), _('WDS')));
1027
1028                                         mode.write = function(section_id, value) {
1029                                                 switch (value) {
1030                                                 case 'ap-wds':
1031                                                         uci.set('wireless', section_id, 'mode', 'ap');
1032                                                         uci.set('wireless', section_id, 'wds', '1');
1033                                                         break;
1034
1035                                                 case 'sta-wds':
1036                                                         uci.set('wireless', section_id, 'mode', 'sta');
1037                                                         uci.set('wireless', section_id, 'wds', '1');
1038                                                         break;
1039
1040                                                 default:
1041                                                         uci.set('wireless', section_id, 'mode', value);
1042                                                         uci.unset('wireless', section_id, 'wds');
1043                                                         break;
1044                                                 }
1045                                         };
1046
1047                                         mode.cfgvalue = function(section_id) {
1048                                                 var mode = uci.get('wireless', section_id, 'mode'),
1049                                                     wds = uci.get('wireless', section_id, 'wds');
1050
1051                                                 if (mode == 'ap' && wds)
1052                                                         return 'ap-wds';
1053                                                 else if (mode == 'sta' && wds)
1054                                                         return 'sta-wds';
1055
1056                                                 return mode;
1057                                         };
1058
1059                                         o = ss.taboption('general', form.Flag, 'hidden', _('Hide <abbr title="Extended Service Set Identifier">ESSID</abbr>'));
1060                                         o.depends('mode', 'ap');
1061                                         o.depends('mode', 'ap-wds');
1062
1063                                         o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'));
1064                                         o.depends('mode', 'ap');
1065                                         o.depends('mode', 'ap-wds');
1066                                         o.default = o.enabled;
1067
1068                                         o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication'));
1069                                         o.depends('mode', 'ap');
1070                                         o.depends('mode', 'ap-wds');
1071
1072                                         o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name'));
1073                                         o.optional = true;
1074                                         o.placeholder = radioNet.getIfname();
1075                                         if (/^radio\d+\.network/.test(o.placeholder))
1076                                                 o.placeholder = '';
1077
1078                                         o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble'));
1079                                         o.default = o.enabled;
1080
1081                                         o = ss.taboption('advanced', form.Value, 'dtim_period', _('DTIM Interval'), _('Delivery Traffic Indication Message Interval'));
1082                                         o.optional = true;
1083                                         o.placeholder = 2;
1084                                         o.datatype = 'range(1,255)';
1085
1086                                         o = ss.taboption('advanced', form.Value, 'wpa_group_rekey', _('Time interval for rekeying GTK'), _('sec'));
1087                                         o.optional    = true;
1088                                         o.placeholder = 600;
1089                                         o.datatype    = 'uinteger';
1090
1091                                         o = ss.taboption('advanced', form.Flag , 'skip_inactivity_poll', _('Disable Inactivity Polling'));
1092                                         o.optional    = true;
1093                                         o.datatype    = 'uinteger';
1094
1095                                         o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('sec'));
1096                                         o.optional    = true;
1097                                         o.placeholder = 300;
1098                                         o.datatype    = 'uinteger';
1099
1100                                         o = ss.taboption('advanced', form.Value, 'max_listen_interval', _('Maximum allowed Listen Interval'));
1101                                         o.optional    = true;
1102                                         o.placeholder = 65535;
1103                                         o.datatype    = 'uinteger';
1104
1105                                         o = ss.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate On Low Acknowledgement'), _('Allow AP mode to disconnect STAs based on low ACK condition'));
1106                                         o.default = o.enabled;
1107                                 }
1108
1109
1110                                 encr = o = ss.taboption('encryption', form.ListValue, 'encryption', _('Encryption'));
1111                                 o.depends('mode', 'ap');
1112                                 o.depends('mode', 'sta');
1113                                 o.depends('mode', 'adhoc');
1114                                 o.depends('mode', 'ahdemo');
1115                                 o.depends('mode', 'ap-wds');
1116                                 o.depends('mode', 'sta-wds');
1117                                 o.depends('mode', 'mesh');
1118
1119                                 o.cfgvalue = function(section_id) {
1120                                         var v = String(uci.get('wireless', section_id, 'encryption'));
1121                                         if (v == 'wep')
1122                                                 return 'wep-open';
1123                                         else if (v.match(/\+/))
1124                                                 return v.replace(/\+.+$/, '');
1125                                         return v;
1126                                 };
1127
1128                                 o.write = function(section_id, value) {
1129                                         var e = this.section.children.filter(function(o) { return o.option == 'encryption' })[0].formvalue(section_id),
1130                                             co = this.section.children.filter(function(o) { return o.option == 'cipher' })[0], c = co.formvalue(section_id);
1131
1132                                         if (value == 'wpa' || value == 'wpa2' || value == 'wpa3' || value == 'wpa3-mixed')
1133                                                 uci.unset('wireless', section_id, 'key');
1134
1135                                         if (co.isActive(section_id) && e && (c == 'tkip' || c == 'ccmp' || c == 'tkip+ccmp'))
1136                                                 e += '+' + c;
1137
1138                                         uci.set('wireless', section_id, 'encryption', e);
1139                                 };
1140
1141                                 o = ss.taboption('encryption', form.ListValue, 'cipher', _('Cipher'));
1142                                 o.depends('encryption', 'wpa');
1143                                 o.depends('encryption', 'wpa2');
1144                                 o.depends('encryption', 'wpa3');
1145                                 o.depends('encryption', 'wpa3-mixed');
1146                                 o.depends('encryption', 'psk');
1147                                 o.depends('encryption', 'psk2');
1148                                 o.depends('encryption', 'wpa-mixed');
1149                                 o.depends('encryption', 'psk-mixed');
1150                                 o.value('auto', _('auto'));
1151                                 o.value('ccmp', _('Force CCMP (AES)'));
1152                                 o.value('tkip', _('Force TKIP'));
1153                                 o.value('tkip+ccmp', _('Force TKIP and CCMP (AES)'));
1154                                 o.write = ss.children.filter(function(o) { return o.option == 'encryption' })[0].write;
1155
1156                                 o.cfgvalue = function(section_id) {
1157                                         var v = String(uci.get('wireless', section_id, 'encryption'));
1158                                         if (v.match(/\+/)) {
1159                                                 v = v.replace(/^[^+]+\+/, '');
1160                                                 if (v == 'aes')
1161                                                         v = 'ccmp';
1162                                                 else if (v == 'tkip+aes' || v == 'aes+tkip' || v == 'ccmp+tkip')
1163                                                         v = 'tkip+ccmp';
1164                                         }
1165                                         return v;
1166                                 };
1167
1168
1169                                 var crypto_modes = [];
1170
1171                                 if (hwtype == 'mac80211') {
1172                                         var has_supplicant = L.hasSystemFeature('wpasupplicant'),
1173                                             has_hostapd = L.hasSystemFeature('hostapd');
1174
1175                                         // Probe EAP support
1176                                         var has_ap_eap = L.hasSystemFeature('hostapd', 'eap'),
1177                                             has_sta_eap = L.hasSystemFeature('wpasupplicant', 'eap');
1178
1179                                         // Probe SAE support
1180                                         var has_ap_sae = L.hasSystemFeature('hostapd', 'sae'),
1181                                             has_sta_sae = L.hasSystemFeature('wpasupplicant', 'sae');
1182
1183                                         // Probe OWE support
1184                                         var has_ap_owe = L.hasSystemFeature('hostapd', 'owe'),
1185                                             has_sta_owe = L.hasSystemFeature('wpasupplicant', 'owe');
1186
1187                                         // Probe Suite-B support
1188                                         var has_ap_eap192 = L.hasSystemFeature('hostapd', 'suiteb192'),
1189                                             has_sta_eap192 = L.hasSystemFeature('wpasupplicant', 'suiteb192');
1190
1191
1192                                         if (has_hostapd || has_supplicant) {
1193                                                 crypto_modes.push(['psk2',      'WPA2-PSK',                    35]);
1194                                                 crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1195                                                 crypto_modes.push(['psk',       'WPA-PSK',                     21]);
1196                                         }
1197                                         else {
1198                                                 encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.');
1199                                         }
1200
1201                                         if (has_ap_sae || has_sta_sae) {
1202                                                 crypto_modes.push(['sae',       'WPA3-SAE',                     31]);
1203                                                 crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]);
1204                                         }
1205
1206                                         if (has_ap_eap || has_sta_eap) {
1207                                                 if (has_ap_eap192 || has_sta_eap192) {
1208                                                         crypto_modes.push(['wpa3', 'WPA3-EAP', 33]);
1209                                                         crypto_modes.push(['wpa3-mixed', 'WPA2-EAP/WPA3-EAP Mixed Mode', 32]);
1210                                                 }
1211
1212                                                 crypto_modes.push(['wpa2', 'WPA2-EAP', 34]);
1213                                                 crypto_modes.push(['wpa',  'WPA-EAP',  20]);
1214                                         }
1215
1216                                         if (has_ap_owe || has_sta_owe) {
1217                                                 crypto_modes.push(['owe', 'OWE', 1]);
1218                                         }
1219
1220                                         encr.crypto_support = {
1221                                                 'ap': {
1222                                                         'wep-open': true,
1223                                                         'wep-shared': true,
1224                                                         'psk': has_hostapd || _('Requires hostapd'),
1225                                                         'psk2': has_hostapd || _('Requires hostapd'),
1226                                                         'psk-mixed': has_hostapd || _('Requires hostapd'),
1227                                                         'sae': has_ap_sae || _('Requires hostapd with SAE support'),
1228                                                         'sae-mixed': has_ap_sae || _('Requires hostapd with SAE support'),
1229                                                         'wpa': has_ap_eap || _('Requires hostapd with EAP support'),
1230                                                         'wpa2': has_ap_eap || _('Requires hostapd with EAP support'),
1231                                                         'wpa3': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1232                                                         'wpa3-mixed': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1233                                                         'owe': has_ap_owe || _('Requires hostapd with OWE support')
1234                                                 },
1235                                                 'sta': {
1236                                                         'wep-open': true,
1237                                                         'wep-shared': true,
1238                                                         'psk': has_supplicant || _('Requires wpa-supplicant'),
1239                                                         'psk2': has_supplicant || _('Requires wpa-supplicant'),
1240                                                         'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1241                                                         'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1242                                                         'sae-mixed': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1243                                                         'wpa': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1244                                                         'wpa2': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1245                                                         'wpa3': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1246                                                         'wpa3-mixed': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1247                                                         'owe': has_sta_owe || _('Requires wpa-supplicant with OWE support')
1248                                                 },
1249                                                 'adhoc': {
1250                                                         'wep-open': true,
1251                                                         'wep-shared': true,
1252                                                         'psk': has_supplicant || _('Requires wpa-supplicant'),
1253                                                         'psk2': has_supplicant || _('Requires wpa-supplicant'),
1254                                                         'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1255                                                 },
1256                                                 'mesh': {
1257                                                         'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support')
1258                                                 },
1259                                                 'ahdemo': {
1260                                                         'wep-open': true,
1261                                                         'wep-shared': true
1262                                                 },
1263                                                 'wds': {
1264                                                         'wep-open': true,
1265                                                         'wep-shared': true
1266                                                 }
1267                                         };
1268
1269                                         encr.crypto_support['ap-wds'] = encr.crypto_support['ap'];
1270                                         encr.crypto_support['sta-wds'] = encr.crypto_support['sta'];
1271
1272                                         encr.validate = function(section_id, value) {
1273                                                 var modeopt = this.section.children.filter(function(o) { return o.option == 'mode' })[0],
1274                                                     modeval = modeopt.formvalue(section_id),
1275                                                     modetitle = modeopt.vallist[modeopt.keylist.indexOf(modeval)],
1276                                                     enctitle = this.vallist[this.keylist.indexOf(value)];
1277
1278                                                 if (value == 'none')
1279                                                         return true;
1280
1281                                                 if (!L.isObject(this.crypto_support[modeval]) || !this.crypto_support[modeval].hasOwnProperty(value))
1282                                                         return _('The selected %s mode is incompatible with %s encryption').format(modetitle, enctitle);
1283
1284                                                 return this.crypto_support[modeval][value];
1285                                         };
1286                                 }
1287                                 else if (hwtype == 'broadcom') {
1288                                         crypto_modes.push(['psk2',     'WPA2-PSK',                    33]);
1289                                         crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1290                                         crypto_modes.push(['psk',      'WPA-PSK',                     21]);
1291                                 }
1292
1293                                 crypto_modes.push(['wep-open',   _('WEP Open System'), 11]);
1294                                 crypto_modes.push(['wep-shared', _('WEP Shared Key'),  10]);
1295                                 crypto_modes.push(['none',       _('No Encryption'),   0]);
1296
1297                                 crypto_modes.sort(function(a, b) { return b[2] - a[2] });
1298
1299                                 for (var i = 0; i < crypto_modes.length; i++) {
1300                                         var security_level = (crypto_modes[i][2] >= 30) ? _('strong security')
1301                                                 : (crypto_modes[i][2] >= 20) ? _('medium security')
1302                                                         : (crypto_modes[i][2] >= 10) ? _('weak security') : _('open network');
1303
1304                                         encr.value(crypto_modes[i][0], '%s (%s)'.format(crypto_modes[i][1], security_level));
1305                                 }
1306
1307
1308                                 o = ss.taboption('encryption', form.Value, 'auth_server', _('Radius-Authentication-Server'));
1309                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1310                                 o.rmempty = true;
1311                                 o.datatype = 'host(0)';
1312
1313                                 o = ss.taboption('encryption', form.Value, 'auth_port', _('Radius-Authentication-Port'), _('Default %d').format(1812));
1314                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1315                                 o.rmempty = true;
1316                                 o.datatype = 'port';
1317
1318                                 o = ss.taboption('encryption', form.Value, 'auth_secret', _('Radius-Authentication-Secret'));
1319                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1320                                 o.rmempty = true;
1321                                 o.password = true;
1322
1323                                 o = ss.taboption('encryption', form.Value, 'acct_server', _('Radius-Accounting-Server'));
1324                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1325                                 o.rmempty = true;
1326                                 o.datatype = 'host(0)';
1327
1328                                 o = ss.taboption('encryption', form.Value, 'acct_port', _('Radius-Accounting-Port'), _('Default %d').format(1813));
1329                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1330                                 o.rmempty = true;
1331                                 o.datatype = 'port';
1332
1333                                 o = ss.taboption('encryption', form.Value, 'acct_secret', _('Radius-Accounting-Secret'));
1334                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1335                                 o.rmempty = true;
1336                                 o.password = true;
1337
1338                                 o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'));
1339                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1340                                 o.rmempty = true;
1341                                 o.datatype = 'host(0)';
1342
1343                                 o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Default %d').format(3799));
1344                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1345                                 o.rmempty = true;
1346                                 o.datatype = 'port';
1347
1348                                 o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'));
1349                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1350                                 o.rmempty = true;
1351                                 o.password = true;
1352
1353
1354                                 o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key'));
1355                                 o.depends('encryption', 'psk');
1356                                 o.depends('encryption', 'psk2');
1357                                 o.depends('encryption', 'psk+psk2');
1358                                 o.depends('encryption', 'psk-mixed');
1359                                 o.depends('encryption', 'sae');
1360                                 o.depends('encryption', 'sae-mixed');
1361                                 o.datatype = 'wpakey';
1362                                 o.rmempty = true;
1363                                 o.password = true;
1364
1365                                 o.cfgvalue = function(section_id) {
1366                                         var key = uci.get('wireless', section_id, 'key');
1367                                         return /^[1234]$/.test(key) ? null : key;
1368                                 };
1369
1370                                 o.write = function(section_id, value) {
1371                                         uci.set('wireless', section_id, 'key', value);
1372                                         uci.unset('wireless', section_id, 'key1');
1373                                         uci.unset('wireless', section_id, 'key2');
1374                                         uci.unset('wireless', section_id, 'key3');
1375                                         uci.unset('wireless', section_id, 'key4');
1376                                 };
1377
1378
1379                                 o = ss.taboption('encryption', form.ListValue, '_wep_key', _('Used Key Slot'));
1380                                 o.depends('encryption', 'wep-open');
1381                                 o.depends('encryption', 'wep-shared');
1382                                 o.value('1', _('Key #%d').format(1));
1383                                 o.value('2', _('Key #%d').format(2));
1384                                 o.value('3', _('Key #%d').format(3));
1385                                 o.value('4', _('Key #%d').format(4));
1386
1387                                 o.cfgvalue = function(section_id) {
1388                                         var slot = +uci.get('wireless', section_id, 'key');
1389                                         return (slot >= 1 && slot <= 4) ? String(slot) : '';
1390                                 };
1391
1392                                 o.write = function(section_id, value) {
1393                                         uci.set('wireless', section_id, 'key', value);
1394                                 };
1395
1396                                 for (var slot = 1; slot <= 4; slot++) {
1397                                         o = ss.taboption('encryption', form.Value, 'key%d'.format(slot), _('Key #%d').format(slot));
1398                                         o.depends('encryption', 'wep-open');
1399                                         o.depends('encryption', 'wep-shared');
1400                                         o.datatype = 'wepkey';
1401                                         o.rmempty = true;
1402                                         o.password = true;
1403
1404                                         o.write = function(section_id, value) {
1405                                                 if (value != null && (value.length == 5 || value.length == 13))
1406                                                         value = 's:%s'.format(value);
1407                                                 uci.set('wireless', section_id, this.option, value);
1408                                         };
1409                                 }
1410
1411
1412                                 if (hwtype == 'mac80211') {
1413                                         // Probe 802.11r support (and EAP support as a proxy for Openwrt)
1414                                         var has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap');
1415
1416                                         o = ss.taboption('encryption', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain'));
1417                                         add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1418                                         if (has_80211r)
1419                                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] });
1420                                         o.rmempty = true;
1421
1422                                         o = ss.taboption('encryption', form.Value, 'nasid', _('NAS ID'), _('Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.'));
1423                                         add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1424                                         o.depends({ ieee80211r: '1' });
1425                                         o.rmempty = true;
1426
1427                                         o = ss.taboption('encryption', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID'));
1428                                         o.depends({ ieee80211r: '1' });
1429                                         o.placeholder = '4f57';
1430                                         o.datatype = 'and(hexstring,length(4))';
1431                                         o.rmempty = true;
1432
1433                                         o = ss.taboption('encryption', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]'));
1434                                         o.depends({ ieee80211r: '1' });
1435                                         o.placeholder = '1000';
1436                                         o.datatype = 'range(1000,65535)';
1437                                         o.rmempty = true;
1438
1439                                         o = ss.taboption('encryption', form.ListValue, 'ft_over_ds', _('FT protocol'));
1440                                         o.depends({ ieee80211r: '1' });
1441                                         o.value('1', _('FT over DS'));
1442                                         o.value('0', _('FT over the Air'));
1443                                         o.rmempty = true;
1444
1445                                         o = ss.taboption('encryption', form.Flag, 'ft_psk_generate_local', _('Generate PMK locally'), _('When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.'));
1446                                         o.depends({ ieee80211r: '1' });
1447                                         o.default = o.enabled;
1448                                         o.rmempty = false;
1449
1450                                         o = ss.taboption('encryption', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes'));
1451                                         o.depends({ ieee80211r: '1' });
1452                                         o.placeholder = '10000';
1453                                         o.datatype = 'uinteger';
1454                                         o.rmempty = true;
1455
1456                                         o = ss.taboption('encryption', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons'));
1457                                         o.depends({ ieee80211r: '1' });
1458                                         o.placeholder = '00004f577274';
1459                                         o.datatype = 'and(hexstring,length(12))';
1460                                         o.rmempty = true;
1461
1462                                         o = ss.taboption('encryption', form.Flag, 'pmk_r1_push', _('PMK R1 Push'));
1463                                         o.depends({ ieee80211r: '1' });
1464                                         o.placeholder = '0';
1465                                         o.rmempty = true;
1466
1467                                         o = ss.taboption('encryption', form.DynamicList, 'r0kh', _('External R0 Key Holder List'), _('List of R0KHs in the same Mobility Domain. <br />Format: MAC-address,NAS-Identifier,128-bit key as hex string. <br />This list is used to map R0KH-ID (NAS Identifier) to a destination MAC address when requesting PMK-R1 key from the R0KH that the STA used during the Initial Mobility Domain Association.'));
1468                                         o.depends({ ieee80211r: '1' });
1469                                         o.rmempty = true;
1470
1471                                         o = ss.taboption('encryption', form.DynamicList, 'r1kh', _('External R1 Key Holder List'), _ ('List of R1KHs in the same Mobility Domain. <br />Format: MAC-address,R1KH-ID as 6 octets with colons,128-bit key as hex string. <br />This list is used to map R1KH-ID to a destination MAC address when sending PMK-R1 key from the R0KH. This is also the list of authorized R1KHs in the MD that can request PMK-R1 keys.'));
1472                                         o.depends({ ieee80211r: '1' });
1473                                         o.rmempty = true;
1474                                         // End of 802.11r options
1475
1476                                         o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method'));
1477                                         o.value('tls',  'TLS');
1478                                         o.value('ttls', 'TTLS');
1479                                         o.value('peap', 'PEAP');
1480                                         o.value('fast', 'FAST');
1481                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1482
1483                                         o = ss.taboption('encryption', form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle,<br />requires the \"ca-bundle\" package"));
1484                                         o.enabled = '1';
1485                                         o.disabled = '0';
1486                                         o.default = o.disabled;
1487                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1488                                         o.validate = function(section_id, value) {
1489                                                 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1490                                                         return _("This option cannot be used because the ca-bundle package is not installed.");
1491                                                 }
1492                                                 return true;
1493                                         };
1494
1495                                         o = ss.taboption('encryption', form.FileUpload, 'ca_cert', _('Path to CA-Certificate'));
1496                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], ca_cert_usesystem: ['0'] });
1497
1498                                         o = ss.taboption('encryption', form.Value, 'subject_match', _('Certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com<br />See `logread -f` during handshake for actual values"));
1499                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1500
1501                                         o = ss.taboption('encryption', form.DynamicList, 'altsubject_match', _('Certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values<br />(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com"));
1502                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1503
1504                                         o = ss.taboption('encryption', form.DynamicList, 'domain_match', _('Certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (exact match)"));
1505                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1506
1507                                         o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match', _('Certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (suffix match)"));
1508                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1509
1510                                         o = ss.taboption('encryption', form.FileUpload, 'client_cert', _('Path to Client-Certificate'));
1511                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1512
1513                                         o = ss.taboption('encryption', form.FileUpload, 'priv_key', _('Path to Private Key'));
1514                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1515
1516                                         o = ss.taboption('encryption', form.Value, 'priv_key_pwd', _('Password of Private Key'));
1517                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1518                                         o.password = true;
1519
1520                                         o = ss.taboption('encryption', form.ListValue, 'auth', _('Authentication'));
1521                                         o.value('PAP', 'PAP');
1522                                         o.value('CHAP', 'CHAP');
1523                                         o.value('MSCHAP', 'MSCHAP');
1524                                         o.value('MSCHAPV2', 'MSCHAPv2');
1525                                         o.value('EAP-GTC', 'EAP-GTC');
1526                                         o.value('EAP-MD5', 'EAP-MD5');
1527                                         o.value('EAP-MSCHAPV2', 'EAP-MSCHAPv2');
1528                                         o.value('EAP-TLS', 'EAP-TLS');
1529                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1530
1531                                         o.validate = function(section_id, value) {
1532                                                 var eo = this.section.children.filter(function(o) { return o.option == 'eap_type' })[0],
1533                                                     ev = eo.formvalue(section_id);
1534
1535                                                 if (ev != 'ttls' && (value == 'PAP' || value == 'CHAP' || value == 'MSCHAP' || value == 'MSCHAPV2'))
1536                                                         return _('This authentication type is not applicable to the selected EAP method.');
1537
1538                                                 return true;
1539                                         };
1540
1541                                         o = ss.taboption('encryption', form.Flag, 'ca_cert2_usesystem', _('Use system certificates for inner-tunnel'), _("Validate server certificate using built-in system CA bundle,<br />requires the \"ca-bundle\" package"));
1542                                         o.enabled = '1';
1543                                         o.disabled = '0';
1544                                         o.default = o.disabled;
1545                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1546                                         o.validate = function(section_id, value) {
1547                                                 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1548                                                         return _("This option cannot be used because the ca-bundle package is not installed.");
1549                                                 }
1550                                                 return true;
1551                                         };
1552
1553                                         o = ss.taboption('encryption', form.FileUpload, 'ca_cert2', _('Path to inner CA-Certificate'));
1554                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'], ca_cert2_usesystem: ['0'] });
1555
1556                                         o = ss.taboption('encryption', form.Value, 'subject_match2', _('Inner certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com<br />See `logread -f` during handshake for actual values"));
1557                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1558
1559                                         o = ss.taboption('encryption', form.DynamicList, 'altsubject_match2', _('Inner certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values<br />(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com"));
1560                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1561
1562                                         o = ss.taboption('encryption', form.DynamicList, 'domain_match2', _('Inner certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (exact match)"));
1563                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1564
1565                                         o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match2', _('Inner certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (suffix match)"));
1566                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1567
1568                                         o = ss.taboption('encryption', form.FileUpload, 'client_cert2', _('Path to inner Client-Certificate'));
1569                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1570
1571                                         o = ss.taboption('encryption', form.FileUpload, 'priv_key2', _('Path to inner Private Key'));
1572                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1573
1574                                         o = ss.taboption('encryption', form.Value, 'priv_key2_pwd', _('Password of inner Private Key'));
1575                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1576                                         o.password = true;
1577
1578                                         o = ss.taboption('encryption', form.Value, 'identity', _('Identity'));
1579                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1580
1581                                         o = ss.taboption('encryption', form.Value, 'anonymous_identity', _('Anonymous Identity'));
1582                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1583
1584                                         o = ss.taboption('encryption', form.Value, 'password', _('Password'));
1585                                         add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1586                                         o.password = true;
1587
1588
1589                                         if (hwtype == 'mac80211') {
1590                                                 // ieee802.11w options
1591                                                 if (L.hasSystemFeature('hostapd', '11w')) {
1592                                                         o = ss.taboption('encryption', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection'), _("Requires the 'full' version of wpad/hostapd and support from the wifi driver <br />(as of Jan 2019: ath9k, ath10k, mwlwifi and mt76)"));
1593                                                         o.value('', _('Disabled'));
1594                                                         o.value('1', _('Optional'));
1595                                                         o.value('2', _('Required'));
1596                                                         add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1597
1598                                                         o.defaults = {
1599                                                                 '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }],
1600                                                                 '1': [{ encryption: 'sae-mixed'}],
1601                                                                 '':  []
1602                                                         };
1603
1604                                                         o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout'));
1605                                                         o.depends('ieee80211w', '1');
1606                                                         o.depends('ieee80211w', '2');
1607                                                         o.datatype = 'uinteger';
1608                                                         o.placeholder = '1000';
1609                                                         o.rmempty = true;
1610
1611                                                         o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout'));
1612                                                         o.depends('ieee80211w', '1');
1613                                                         o.depends('ieee80211w', '2');
1614                                                         o.datatype = 'uinteger';
1615                                                         o.placeholder = '201';
1616                                                         o.rmempty = true;
1617                                                 };
1618
1619                                                 o = ss.taboption('encryption', form.Flag, 'wpa_disable_eapol_key_retries', _('Enable key reinstallation (KRACK) countermeasures'), _('Complicates key reinstallation attacks on the client side by disabling retransmission of EAPOL-Key frames that are used to install keys. This workaround might cause interoperability issues and reduced robustness of key negotiation especially in environments with heavy traffic load.'));
1620                                                 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1621
1622                                                 if (L.hasSystemFeature('hostapd', 'cli') && L.hasSystemFeature('wpasupplicant')) {
1623                                                         o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE'))
1624                                                         o.enabled = '1';
1625                                                         o.disabled = '0';
1626                                                         o.default = o.disabled;
1627                                                         o.depends('encryption', 'psk');
1628                                                         o.depends('encryption', 'psk2');
1629                                                         o.depends('encryption', 'psk-mixed');
1630                                                         o.depends('encryption', 'sae');
1631                                                         o.depends('encryption', 'sae-mixed');
1632                                                 }
1633                                         }
1634                                 }
1635                         });
1636                 };
1637
1638                 s.handleRemove = function(section_id, ev) {
1639                         document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5;
1640                         return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]);
1641                 };
1642
1643                 s.handleScan = function(radioDev, ev) {
1644                         var table = E('div', { 'class': 'table' }, [
1645                                 E('div', { 'class': 'tr table-titles' }, [
1646                                         E('div', { 'class': 'th col-2 middle center' }, _('Signal')),
1647                                         E('div', { 'class': 'th col-4 middle left' }, _('SSID')),
1648                                         E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
1649                                         E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
1650                                         E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')),
1651                                         E('div', { 'class': 'th col-3 middle left' }, _('Encryption')),
1652                                         E('div', { 'class': 'th cbi-section-actions right' }, ' '),
1653                                 ])
1654                         ]);
1655
1656                         var stop = E('button', {
1657                                 'class': 'btn',
1658                                 'click': L.bind(this.handleScanStartStop, this),
1659                                 'style': 'display:none',
1660                                 'data-state': 'stop'
1661                         }, _('Stop refresh'));
1662
1663                         cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
1664
1665                         var md = ui.showModal(_('Join Network: Wireless Scan'), [
1666                                 table,
1667                                 E('div', { 'class': 'right' }, [
1668                                         stop,
1669                                         ' ',
1670                                         E('button', {
1671                                                 'class': 'btn',
1672                                                 'click': L.bind(this.handleScanAbort, this)
1673                                         }, _('Dismiss'))
1674                                 ])
1675                         ]);
1676
1677                         md.style.maxWidth = '90%';
1678                         md.style.maxHeight = 'none';
1679
1680                         this.pollFn = L.bind(this.handleScanRefresh, this, radioDev, {}, table, stop);
1681
1682                         poll.add(this.pollFn);
1683                         poll.start();
1684                 };
1685
1686                 s.handleScanRefresh = function(radioDev, scanCache, table, stop) {
1687                         return radioDev.getScanList().then(L.bind(function(results) {
1688                                 var rows = [];
1689
1690                                 for (var i = 0; i < results.length; i++)
1691                                         scanCache[results[i].bssid] = results[i];
1692
1693                                 for (var k in scanCache)
1694                                         if (scanCache[k].stale)
1695                                                 results.push(scanCache[k]);
1696
1697                                 results.sort(function(a, b) {
1698                                         var diff = (b.quality - a.quality) || (a.channel - b.channel);
1699
1700                                         if (diff)
1701                                                 return diff;
1702
1703                                         if (a.ssid < b.ssid)
1704                                                 return -1;
1705                                         else if (a.ssid > b.ssid)
1706                                                 return 1;
1707
1708                                         if (a.bssid < b.bssid)
1709                                                 return -1;
1710                                         else if (a.bssid > b.bssid)
1711                                                 return 1;
1712                                 });
1713
1714                                 for (var i = 0; i < results.length; i++) {
1715                                         var res = results[i],
1716                                             qv = res.quality || 0,
1717                                             qm = res.quality_max || 0,
1718                                             q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
1719                                             s = res.stale ? 'opacity:0.5' : '';
1720
1721                                         rows.push([
1722                                                 E('span', { 'style': s }, render_signal_badge(q, res.signal, res.noise)),
1723                                                 E('span', { 'style': s }, (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))),
1724                                                 E('span', { 'style': s }, '%d'.format(res.channel)),
1725                                                 E('span', { 'style': s }, '%h'.format(res.mode)),
1726                                                 E('span', { 'style': s }, '%h'.format(res.bssid)),
1727                                                 E('span', { 'style': s }, '%h'.format(network.formatWifiEncryption(res.encryption))),
1728                                                 E('div', { 'class': 'right' }, E('button', {
1729                                                         'class': 'cbi-button cbi-button-action important',
1730                                                         'click': L.bind(this.handleJoin, this, radioDev, res)
1731                                                 }, _('Join Network')))
1732                                         ]);
1733
1734                                         res.stale = true;
1735                                 }
1736
1737                                 cbi_update_table(table, rows);
1738
1739                                 stop.disabled = false;
1740                                 stop.style.display = '';
1741                                 stop.classList.remove('spinning');
1742                         }, this));
1743                 };
1744
1745                 s.handleScanStartStop = function(ev) {
1746                         var btn = ev.currentTarget;
1747
1748                         if (btn.getAttribute('data-state') == 'stop') {
1749                                 poll.remove(this.pollFn);
1750                                 btn.firstChild.data = _('Start refresh');
1751                                 btn.setAttribute('data-state', 'start');
1752                         }
1753                         else {
1754                                 poll.add(this.pollFn);
1755                                 btn.firstChild.data = _('Stop refresh');
1756                                 btn.setAttribute('data-state', 'stop');
1757                                 btn.classList.add('spinning');
1758                                 btn.disabled = true;
1759                         }
1760                 };
1761
1762                 s.handleScanAbort = function(ev) {
1763                         var md = dom.parent(ev.target, 'div[aria-modal="true"]');
1764                         if (md) {
1765                                 md.style.maxWidth = '';
1766                                 md.style.maxHeight = '';
1767                         }
1768
1769                         ui.hideModal();
1770                         poll.remove(this.pollFn);
1771
1772                         this.pollFn = null;
1773                 };
1774
1775                 s.handleJoinConfirm = function(radioDev, bss, form, ev) {
1776                         var nameopt = L.toArray(form.lookupOption('name', '_new_'))[0],
1777                             passopt = L.toArray(form.lookupOption('password', '_new_'))[0],
1778                             bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0],
1779                             zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0],
1780                             replopt = L.toArray(form.lookupOption('replace', '_new_'))[0],
1781                             nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null,
1782                             passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null,
1783                             bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null,
1784                             zoneval = zoneopt ? zoneopt.formvalue('_new_') : null,
1785                             enc = L.isObject(bss.encryption) ? bss.encryption : null,
1786                             is_wep = (enc && Array.isArray(enc.wep)),
1787                             is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' })),
1788                             is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }));
1789
1790                         if (nameval == null || (passopt && passval == null))
1791                                 return;
1792
1793                         var section_id = null;
1794
1795                         return this.map.save(function() {
1796                                 var wifi_sections = uci.sections('wireless', 'wifi-iface');
1797
1798                                 if (replopt.formvalue('_new_') == '1') {
1799                                         for (var i = 0; i < wifi_sections.length; i++)
1800                                                 if (wifi_sections[i].device == radioDev.getName())
1801                                                         uci.remove('wireless', wifi_sections[i]['.name']);
1802                                 }
1803
1804                                 if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') {
1805                                         for (var i = 0; i < wifi_sections.length; i++)
1806                                                 if (wifi_sections[i].device == radioDev.getName())
1807                                                         uci.set('wireless', wifi_sections[i]['.name'], 'disabled', '1');
1808
1809                                         uci.unset('wireless', radioDev.getName(), 'disabled');
1810                                 }
1811
1812                                 section_id = next_free_sid(wifi_sections.length);
1813
1814                                 uci.add('wireless', 'wifi-iface', section_id);
1815                                 uci.set('wireless', section_id, 'device', radioDev.getName());
1816                                 uci.set('wireless', section_id, 'mode', (bss.mode == 'Ad-Hoc') ? 'adhoc' : 'sta');
1817                                 uci.set('wireless', section_id, 'network', nameval);
1818
1819                                 if (bss.ssid != null) {
1820                                         uci.set('wireless', section_id, 'ssid', bss.ssid);
1821
1822                                         if (bssidval == '1')
1823                                                 uci.set('wireless', section_id, 'bssid', bss.bssid);
1824                                 }
1825                                 else if (bss.bssid != null) {
1826                                         uci.set('wireless', section_id, 'bssid', bss.bssid);
1827                                 }
1828
1829                                 if (is_sae) {
1830                                         uci.set('wireless', section_id, 'encryption', 'sae');
1831                                         uci.set('wireless', section_id, 'key', passval);
1832                                 }
1833                                 else if (is_psk) {
1834                                         for (var i = enc.wpa.length - 1; i >= 0; i--) {
1835                                                 if (enc.wpa[i] == 2) {
1836                                                         uci.set('wireless', section_id, 'encryption', 'psk2');
1837                                                         break;
1838                                                 }
1839                                                 else if (enc.wpa[i] == 1) {
1840                                                         uci.set('wireless', section_id, 'encryption', 'psk');
1841                                                         break;
1842                                                 }
1843                                         }
1844
1845                                         uci.set('wireless', section_id, 'key', passval);
1846                                 }
1847                                 else if (is_wep) {
1848                                         uci.set('wireless', section_id, 'encryption', 'wep-open');
1849                                         uci.set('wireless', section_id, 'key', '1');
1850                                         uci.set('wireless', section_id, 'key1', passval);
1851                                 }
1852                                 else {
1853                                         uci.set('wireless', section_id, 'encryption', 'none');
1854                                 }
1855
1856                                 return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) {
1857                                         firewall.deleteNetwork(net.getName());
1858
1859                                         var zonePromise = zoneval
1860                                                 ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) })
1861                                                 : Promise.resolve();
1862
1863                                         return zonePromise.then(function(zone) {
1864                                                 if (zone)
1865                                                         zone.addNetwork(net.getName());
1866                                         });
1867                                 });
1868                         }).then(L.bind(function() {
1869                                 return this.renderMoreOptionsModal(section_id);
1870                         }, this));
1871                 };
1872
1873                 s.handleJoin = function(radioDev, bss, ev) {
1874                         this.handleScanAbort(ev);
1875
1876                         var m2 = new form.Map('wireless'),
1877                             s2 = m2.section(form.NamedSection, '_new_'),
1878                             enc = L.isObject(bss.encryption) ? bss.encryption : null,
1879                             is_wep = (enc && Array.isArray(enc.wep)),
1880                             is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae' })),
1881                             replace, passphrase, name, bssid, zone;
1882
1883                         var nameUsed = function(name) {
1884                                 var s = uci.get('network', name);
1885                                 if (s != null && s['.type'] != 'interface')
1886                                         return true;
1887
1888                                 var net = (s != null) ? network.instantiateNetwork(name) : null;
1889                                 return (net != null && !net.isEmpty());
1890                         };
1891
1892                         s2.render = function() {
1893                                 return Promise.all([
1894                                         {},
1895                                         this.renderUCISection('_new_')
1896                                 ]).then(this.renderContents.bind(this));
1897                         };
1898
1899                         replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.'));
1900
1901                         name = s2.option(form.Value, 'name', _('Name of the new network'), _('The allowed characters are: <code>A-Z</code>, <code>a-z</code>, <code>0-9</code> and <code>_</code>'));
1902                         name.datatype = 'uciname';
1903                         name.default = 'wwan';
1904                         name.rmempty = false;
1905                         name.validate = function(section_id, value) {
1906                                 if (nameUsed(value))
1907                                         return _('The network name is already used');
1908
1909                                 return true;
1910                         };
1911
1912                         for (var i = 2; nameUsed(name.default); i++)
1913                                 name.default = 'wwan%d'.format(i);
1914
1915                         if (is_wep || is_psk) {
1916                                 passphrase = s2.option(form.Value, 'password', is_wep ? _('WEP passphrase') : _('WPA passphrase'), _('Specify the secret encryption key here.'));
1917                                 passphrase.datatype = is_wep ? 'wepkey' : 'wpakey';
1918                                 passphrase.password = true;
1919                                 passphrase.rmempty = false;
1920                         }
1921
1922                         if (bss.ssid != null) {
1923                                 bssid = s2.option(form.Flag, 'bssid', _('Lock to BSSID'), _('Instead of joining any network with a matching SSID, only connect to the BSSID <code>%h</code>.').format(bss.bssid));
1924                                 bssid.default = '0';
1925                         }
1926
1927                         zone = s2.option(widgets.ZoneSelect, 'zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>custom</em> field to define a new zone and attach the interface to it.'));
1928                         zone.default = 'wan';
1929
1930                         return m2.render().then(L.bind(function(nodes) {
1931                                 ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [
1932                                         nodes,
1933                                         E('div', { 'class': 'right' }, [
1934                                                 E('button', {
1935                                                         'class': 'btn',
1936                                                         'click': ui.hideModal
1937                                                 }, _('Cancel')), ' ',
1938                                                 E('button', {
1939                                                         'class': 'cbi-button cbi-button-positive important',
1940                                                         'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2)
1941                                                 }, _('Submit'))
1942                                         ])
1943                                 ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus();
1944                         }, this));
1945                 };
1946
1947                 s.handleAdd = function(radioDev, ev) {
1948                         var section_id = next_free_sid(uci.sections('wireless', 'wifi-iface').length);
1949
1950                         uci.unset('wireless', radioDev.getName(), 'disabled');
1951
1952                         uci.add('wireless', 'wifi-iface', section_id);
1953                         uci.set('wireless', section_id, 'device', radioDev.getName());
1954                         uci.set('wireless', section_id, 'mode', 'ap');
1955                         uci.set('wireless', section_id, 'ssid', 'OpenWrt');
1956                         uci.set('wireless', section_id, 'encryption', 'none');
1957
1958                         this.addedSection = section_id;
1959                         return this.renderMoreOptionsModal(section_id);
1960                 };
1961
1962                 o = s.option(form.DummyValue, '_badge');
1963                 o.modalonly = false;
1964                 o.textvalue = function(section_id) {
1965                         var inst = this.section.lookupRadioOrNetwork(section_id),
1966                             node = E('div', { 'class': 'center' });
1967
1968                         if (inst.getWifiNetworks)
1969                                 node.appendChild(render_radio_badge(inst));
1970                         else
1971                                 node.appendChild(render_network_badge(inst));
1972
1973                         return node;
1974                 };
1975
1976                 o = s.option(form.DummyValue, '_stat');
1977                 o.modalonly = false;
1978                 o.textvalue = function(section_id) {
1979                         var inst = this.section.lookupRadioOrNetwork(section_id);
1980
1981                         if (inst.getWifiNetworks)
1982                                 return render_radio_status(inst, this.section.wifis.filter(function(e) {
1983                                         return (e.getWifiDeviceName() == inst.getName());
1984                                 }));
1985                         else
1986                                 return render_network_status(inst);
1987                 };
1988
1989                 return m.render().then(L.bind(function(m, nodes) {
1990                         poll.add(L.bind(function() {
1991                                 var section_ids = m.children[0].cfgsections(),
1992                                     tasks = [ network.getHostHints(), network.getWifiDevices() ];
1993
1994                                 for (var i = 0; i < section_ids.length; i++) {
1995                                         var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
1996                                             dsc = row.querySelector('[data-name="_stat"] > div'),
1997                                             btns = row.querySelectorAll('.cbi-section-actions button');
1998
1999                                         if (dsc.getAttribute('restart') == '') {
2000                                                 dsc.setAttribute('restart', '1');
2001                                                 tasks.push(fs.exec('/sbin/wifi', ['up', section_ids[i]]).catch(function(e) {
2002                                                         ui.addNotification(null, E('p', e.message));
2003                                                 }));
2004                                         }
2005                                         else if (dsc.getAttribute('restart') == '1') {
2006                                                 dsc.removeAttribute('restart');
2007                                                 btns[0].classList.remove('spinning');
2008                                                 btns[0].disabled = false;
2009                                         }
2010                                 }
2011
2012                                 return Promise.all(tasks)
2013                                         .then(L.bind(function(hosts_radios) {
2014                                                 var tasks = [];
2015
2016                                                 for (var i = 0; i < hosts_radios[1].length; i++)
2017                                                         tasks.push(hosts_radios[1][i].getWifiNetworks());
2018
2019                                                 return Promise.all(tasks).then(function(data) {
2020                                                         hosts_radios[2] = [];
2021
2022                                                         for (var i = 0; i < data.length; i++)
2023                                                                 hosts_radios[2].push.apply(hosts_radios[2], data[i]);
2024
2025                                                         return hosts_radios;
2026                                                 });
2027                                         }, network))
2028                                         .then(L.bind(function(hosts_radios_wifis) {
2029                                                 var tasks = [];
2030
2031                                                 for (var i = 0; i < hosts_radios_wifis[2].length; i++)
2032                                                         tasks.push(hosts_radios_wifis[2][i].getAssocList());
2033
2034                                                 return Promise.all(tasks).then(function(data) {
2035                                                         hosts_radios_wifis[3] = [];
2036
2037                                                         for (var i = 0; i < data.length; i++) {
2038                                                                 var wifiNetwork = hosts_radios_wifis[2][i],
2039                                                                     radioDev = hosts_radios_wifis[1].filter(function(d) { return d.getName() == wifiNetwork.getWifiDeviceName() })[0];
2040
2041                                                                 for (var j = 0; j < data[i].length; j++)
2042                                                                         hosts_radios_wifis[3].push(Object.assign({ radio: radioDev, network: wifiNetwork }, data[i][j]));
2043                                                         }
2044
2045                                                         return hosts_radios_wifis;
2046                                                 });
2047                                         }, network))
2048                                         .then(L.bind(this.poll_status, this, nodes));
2049                         }, this), 5);
2050
2051                         var table = E('div', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
2052                                 E('div', { 'class': 'tr table-titles' }, [
2053                                         E('div', { 'class': 'th nowrap' }, _('Network')),
2054                                         E('div', { 'class': 'th hide-xs' }, _('MAC-Address')),
2055                                         E('div', { 'class': 'th' }, _('Host')),
2056                                         E('div', { 'class': 'th' }, _('Signal / Noise')),
2057                                         E('div', { 'class': 'th' }, _('RX Rate / TX Rate'))
2058                                 ])
2059                         ]);
2060
2061                         cbi_update_table(table, [], E('em', { 'class': 'spinning' }, _('Collecting data...')))
2062
2063                         return E([ nodes, E('h3', _('Associated Stations')), table ]);
2064                 }, this, m));
2065         }
2066 });