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