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