luci-base: Fix typos in JsDocs
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / uci.js
1 'use strict';
2 'require rpc';
3 'require baseclass';
4
5 function isEmpty(object, ignore) {
6 for (var property in object)
7 if (object.hasOwnProperty(property) && property != ignore)
8 return false;
9
10 return true;
11 }
12
13 /**
14 * @class uci
15 * @memberof LuCI
16 * @hideconstructor
17 * @classdesc
18 *
19 * The `LuCI.uci` class utilizes {@link LuCI.rpc} to declare low level
20 * remote UCI `ubus` procedures and implements a local caching and data
21 * manipulation layer on top to allow for synchronous operations on
22 * UCI configuration data.
23 */
24 return baseclass.extend(/** @lends LuCI.uci.prototype */ {
25 __init__: function() {
26 this.state = {
27 newidx: 0,
28 values: { },
29 creates: { },
30 changes: { },
31 deletes: { },
32 reorder: { }
33 };
34
35 this.loaded = {};
36 },
37
38 callLoad: rpc.declare({
39 object: 'uci',
40 method: 'get',
41 params: [ 'config' ],
42 expect: { values: { } },
43 reject: true
44 }),
45
46 callOrder: rpc.declare({
47 object: 'uci',
48 method: 'order',
49 params: [ 'config', 'sections' ],
50 reject: true
51 }),
52
53 callAdd: rpc.declare({
54 object: 'uci',
55 method: 'add',
56 params: [ 'config', 'type', 'name', 'values' ],
57 expect: { section: '' },
58 reject: true
59 }),
60
61 callSet: rpc.declare({
62 object: 'uci',
63 method: 'set',
64 params: [ 'config', 'section', 'values' ],
65 reject: true
66 }),
67
68 callDelete: rpc.declare({
69 object: 'uci',
70 method: 'delete',
71 params: [ 'config', 'section', 'options' ],
72 reject: true
73 }),
74
75 callApply: rpc.declare({
76 object: 'uci',
77 method: 'apply',
78 params: [ 'timeout', 'rollback' ],
79 reject: true
80 }),
81
82 callConfirm: rpc.declare({
83 object: 'uci',
84 method: 'confirm',
85 reject: true
86 }),
87
88
89 /**
90 * Generates a new, unique section ID for the given configuration.
91 *
92 * Note that the generated ID is temporary, it will get replaced by an
93 * identifier in the form `cfgXXXXXX` once the configuration is saved
94 * by the remote `ubus` UCI api.
95 *
96 * @param {string} conf
97 * The configuration to generate the new section ID for.
98 *
99 * @returns {string}
100 * A newly generated, unique section ID in the form `newXXXXXX`
101 * where `X` denotes a hexadecimal digit.
102 */
103 createSID: function(conf) {
104 var v = this.state.values,
105 n = this.state.creates,
106 sid;
107
108 do {
109 sid = "new%06x".format(Math.random() * 0xFFFFFF);
110 } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
111
112 return sid;
113 },
114
115 /**
116 * Resolves a given section ID in extended notation to the internal
117 * section ID value.
118 *
119 * @param {string} conf
120 * The configuration to resolve the section ID for.
121 *
122 * @param {string} sid
123 * The section ID to resolve. If the ID is in the form `@typename[#]`,
124 * it will get resolved to an internal anonymous ID in the forms
125 * `cfgXXXXXX`/`newXXXXXX` or to the name of a section in case it points
126 * to a named section. When the given ID is not in extended notation,
127 * it will be returned as-is.
128 *
129 * @returns {string|null}
130 * Returns the resolved section ID or the original given ID if it was
131 * not in extended notation. Returns `null` when an extended ID could
132 * not be resolved to existing section ID.
133 */
134 resolveSID: function(conf, sid) {
135 if (typeof(sid) != 'string')
136 return sid;
137
138 var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
139
140 if (m) {
141 var type = m[1],
142 pos = +m[2],
143 sections = this.sections(conf, type),
144 section = sections[pos >= 0 ? pos : sections.length + pos];
145
146 return section ? section['.name'] : null;
147 }
148
149 return sid;
150 },
151
152 /* private */
153 reorderSections: function() {
154 var v = this.state.values,
155 n = this.state.creates,
156 r = this.state.reorder,
157 tasks = [];
158
159 if (Object.keys(r).length === 0)
160 return Promise.resolve();
161
162 /*
163 gather all created and existing sections, sort them according
164 to their index value and issue an uci order call
165 */
166 for (var c in r) {
167 var o = [ ];
168
169 if (n[c])
170 for (var s in n[c])
171 o.push(n[c][s]);
172
173 for (var s in v[c])
174 o.push(v[c][s]);
175
176 if (o.length > 0) {
177 o.sort(function(a, b) {
178 return (a['.index'] - b['.index']);
179 });
180
181 var sids = [ ];
182
183 for (var i = 0; i < o.length; i++)
184 sids.push(o[i]['.name']);
185
186 tasks.push(this.callOrder(c, sids));
187 }
188 }
189
190 this.state.reorder = { };
191 return Promise.all(tasks);
192 },
193
194 /* private */
195 loadPackage: function(packageName) {
196 if (this.loaded[packageName] == null)
197 return (this.loaded[packageName] = this.callLoad(packageName));
198
199 return Promise.resolve(this.loaded[packageName]);
200 },
201
202 /**
203 * Loads the given UCI configurations from the remote `ubus` api.
204 *
205 * Loaded configurations are cached and only loaded once. Subsequent
206 * load operations of the same configurations will return the cached
207 * data.
208 *
209 * To force reloading a configuration, it has to be unloaded with
210 * {@link LuCI.uci#unload uci.unload()} first.
211 *
212 * @param {string|string[]} packages
213 * The name of the configuration or an array of configuration
214 * names to load.
215 *
216 * @returns {Promise<string[]>}
217 * Returns a promise resolving to the names of the configurations
218 * that have been successfully loaded.
219 */
220 load: function(packages) {
221 var self = this,
222 pkgs = [ ],
223 tasks = [];
224
225 if (!Array.isArray(packages))
226 packages = [ packages ];
227
228 for (var i = 0; i < packages.length; i++)
229 if (!self.state.values[packages[i]]) {
230 pkgs.push(packages[i]);
231 tasks.push(self.loadPackage(packages[i]));
232 }
233
234 return Promise.all(tasks).then(function(responses) {
235 for (var i = 0; i < responses.length; i++)
236 self.state.values[pkgs[i]] = responses[i];
237
238 if (responses.length)
239 document.dispatchEvent(new CustomEvent('uci-loaded'));
240
241 return pkgs;
242 });
243 },
244
245 /**
246 * Unloads the given UCI configurations from the local cache.
247 *
248 * @param {string|string[]} packages
249 * The name of the configuration or an array of configuration
250 * names to unload.
251 */
252 unload: function(packages) {
253 if (!Array.isArray(packages))
254 packages = [ packages ];
255
256 for (var i = 0; i < packages.length; i++) {
257 delete this.state.values[packages[i]];
258 delete this.state.creates[packages[i]];
259 delete this.state.changes[packages[i]];
260 delete this.state.deletes[packages[i]];
261
262 delete this.loaded[packages[i]];
263 }
264 },
265
266 /**
267 * Adds a new section of the given type to the given configuration,
268 * optionally named according to the given name.
269 *
270 * @param {string} conf
271 * The name of the configuration to add the section to.
272 *
273 * @param {string} type
274 * The type of the section to add.
275 *
276 * @param {string} [name]
277 * The name of the section to add. If the name is omitted, an anonymous
278 * section will be added instead.
279 *
280 * @returns {string}
281 * Returns the section ID of the newly added section which is equivalent
282 * to the given name for non-anonymous sections.
283 */
284 add: function(conf, type, name) {
285 var n = this.state.creates,
286 sid = name || this.createSID(conf);
287
288 if (!n[conf])
289 n[conf] = { };
290
291 n[conf][sid] = {
292 '.type': type,
293 '.name': sid,
294 '.create': name,
295 '.anonymous': !name,
296 '.index': 1000 + this.state.newidx++
297 };
298
299 return sid;
300 },
301
302 /**
303 * Removes the section with the given ID from the given configuration.
304 *
305 * @param {string} conf
306 * The name of the configuration to remove the section from.
307 *
308 * @param {string} sid
309 * The ID of the section to remove.
310 */
311 remove: function(conf, sid) {
312 var v = this.state.values,
313 n = this.state.creates,
314 c = this.state.changes,
315 d = this.state.deletes;
316
317 /* requested deletion of a just created section */
318 if (n[conf] && n[conf][sid]) {
319 delete n[conf][sid];
320 }
321 else if (v[conf] && v[conf][sid]) {
322 if (c[conf])
323 delete c[conf][sid];
324
325 if (!d[conf])
326 d[conf] = { };
327
328 d[conf][sid] = true;
329 }
330 },
331
332 /**
333 * A section object represents the options and their corresponding values
334 * enclosed within a configuration section, as well as some additional
335 * meta data such as sort indexes and internal ID.
336 *
337 * Any internal metadata fields are prefixed with a dot which isn't
338 * an allowed character for normal option names.
339 *
340 * @typedef {Object<string, boolean|number|string|string[]>} SectionObject
341 * @memberof LuCI.uci
342 *
343 * @property {boolean} .anonymous
344 * The `.anonymous` property specifies whether the configuration is
345 * anonymous (`true`) or named (`false`).
346 *
347 * @property {number} .index
348 * The `.index` property specifies the sort order of the section.
349 *
350 * @property {string} .name
351 * The `.name` property holds the name of the section object. It may be
352 * either an anonymous ID in the form `cfgXXXXXX` or `newXXXXXX` with `X`
353 * being a hexadecimal digit or a string holding the name of the section.
354 *
355 * @property {string} .type
356 * The `.type` property contains the type of the corresponding uci
357 * section.
358 *
359 * @property {string|string[]} *
360 * A section object may contain an arbitrary number of further properties
361 * representing the uci option enclosed in the section.
362 *
363 * All option property names will be in the form `[A-Za-z0-9_]+` and
364 * either contain a string value or an array of strings, in case the
365 * underlying option is an UCI list.
366 */
367
368 /**
369 * The sections callback is invoked for each section found within
370 * the given configuration and receives the section object and its
371 * associated name as arguments.
372 *
373 * @callback LuCI.uci~sectionsFn
374 *
375 * @param {LuCI.uci.SectionObject} section
376 * The section object.
377 *
378 * @param {string} sid
379 * The name or ID of the section.
380 */
381
382 /**
383 * Enumerates the sections of the given configuration, optionally
384 * filtered by type.
385 *
386 * @param {string} conf
387 * The name of the configuration to enumerate the sections for.
388 *
389 * @param {string} [type]
390 * Enumerate only sections of the given type. If omitted, enumerate
391 * all sections.
392 *
393 * @param {LuCI.uci~sectionsFn} [cb]
394 * An optional callback to invoke for each enumerated section.
395 *
396 * @returns {Array<LuCI.uci.SectionObject>}
397 * Returns a sorted array of the section objects within the given
398 * configuration, filtered by type of a type has been specified.
399 */
400 sections: function(conf, type, cb) {
401 var sa = [ ],
402 v = this.state.values[conf],
403 n = this.state.creates[conf],
404 c = this.state.changes[conf],
405 d = this.state.deletes[conf];
406
407 if (!v)
408 return sa;
409
410 for (var s in v)
411 if (!d || d[s] !== true)
412 if (!type || v[s]['.type'] == type)
413 sa.push(Object.assign({ }, v[s], c ? c[s] : null));
414
415 if (n)
416 for (var s in n)
417 if (!type || n[s]['.type'] == type)
418 sa.push(Object.assign({ }, n[s]));
419
420 sa.sort(function(a, b) {
421 return a['.index'] - b['.index'];
422 });
423
424 for (var i = 0; i < sa.length; i++)
425 sa[i]['.index'] = i;
426
427 if (typeof(cb) == 'function')
428 for (var i = 0; i < sa.length; i++)
429 cb.call(this, sa[i], sa[i]['.name']);
430
431 return sa;
432 },
433
434 /**
435 * Gets the value of the given option within the specified section
436 * of the given configuration or the entire section object if the
437 * option name is omitted.
438 *
439 * @param {string} conf
440 * The name of the configuration to read the value from.
441 *
442 * @param {string} sid
443 * The name or ID of the section to read.
444 *
445 * @param {string} [opt]
446 * The option name to read the value from. If the option name is
447 * omitted or `null`, the entire section is returned instead.
448 *
449 * @returns {null|string|string[]|LuCI.uci.SectionObject}
450 * - Returns a string containing the option value in case of a
451 * plain UCI option.
452 * - Returns an array of strings containing the option values in
453 * case of `option` pointing to an UCI list.
454 * - Returns a {@link LuCI.uci.SectionObject section object} if
455 * the `option` argument has been omitted or is `null`.
456 * - Returns `null` if the config, section or option has not been
457 * found or if the corresponding configuration is not loaded.
458 */
459 get: function(conf, sid, opt) {
460 var v = this.state.values,
461 n = this.state.creates,
462 c = this.state.changes,
463 d = this.state.deletes;
464
465 sid = this.resolveSID(conf, sid);
466
467 if (sid == null)
468 return null;
469
470 /* requested option in a just created section */
471 if (n[conf] && n[conf][sid]) {
472 if (!n[conf])
473 return null;
474
475 if (opt == null)
476 return n[conf][sid];
477
478 return n[conf][sid][opt];
479 }
480
481 /* requested an option value */
482 if (opt != null) {
483 /* check whether option was deleted */
484 if (d[conf] && d[conf][sid])
485 if (d[conf][sid] === true || d[conf][sid][opt])
486 return null;
487
488 /* check whether option was changed */
489 if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
490 return c[conf][sid][opt];
491
492 /* return base value */
493 if (v[conf] && v[conf][sid])
494 return v[conf][sid][opt];
495
496 return null;
497 }
498
499 /* requested an entire section */
500 if (v[conf]) {
501 /* check whether entire section was deleted */
502 if (d[conf] && d[conf][sid] === true)
503 return null;
504
505 var s = v[conf][sid] || null;
506
507 if (s) {
508 /* merge changes */
509 if (c[conf] && c[conf][sid])
510 for (var opt in c[conf][sid])
511 if (c[conf][sid][opt] != null)
512 s[opt] = c[conf][sid][opt];
513
514 /* merge deletions */
515 if (d[conf] && d[conf][sid])
516 for (var opt in d[conf][sid])
517 delete s[opt];
518 }
519
520 return s;
521 }
522
523 return null;
524 },
525
526 /**
527 * Sets the value of the given option within the specified section
528 * of the given configuration.
529 *
530 * If either config, section or option is null, or if `option` begins
531 * with a dot, the function will do nothing.
532 *
533 * @param {string} conf
534 * The name of the configuration to set the option value in.
535 *
536 * @param {string} sid
537 * The name or ID of the section to set the option value in.
538 *
539 * @param {string} opt
540 * The option name to set the value for.
541 *
542 * @param {null|string|string[]} val
543 * The option value to set. If the value is `null` or an empty string,
544 * the option will be removed, otherwise it will be set or overwritten
545 * with the given value.
546 */
547 set: function(conf, sid, opt, val) {
548 var v = this.state.values,
549 n = this.state.creates,
550 c = this.state.changes,
551 d = this.state.deletes;
552
553 sid = this.resolveSID(conf, sid);
554
555 if (sid == null || opt == null || opt.charAt(0) == '.')
556 return;
557
558 if (n[conf] && n[conf][sid]) {
559 if (val != null)
560 n[conf][sid][opt] = val;
561 else
562 delete n[conf][sid][opt];
563 }
564 else if (val != null && val !== '') {
565 /* do not set within deleted section */
566 if (d[conf] && d[conf][sid] === true)
567 return;
568
569 /* only set in existing sections */
570 if (!v[conf] || !v[conf][sid])
571 return;
572
573 if (!c[conf])
574 c[conf] = {};
575
576 if (!c[conf][sid])
577 c[conf][sid] = {};
578
579 /* undelete option */
580 if (d[conf] && d[conf][sid]) {
581 if (isEmpty(d[conf][sid], opt))
582 delete d[conf][sid];
583 else
584 delete d[conf][sid][opt];
585 }
586
587 c[conf][sid][opt] = val;
588 }
589 else {
590 /* revert any change for to-be-deleted option */
591 if (c[conf] && c[conf][sid]) {
592 if (isEmpty(c[conf][sid], opt))
593 delete c[conf][sid];
594 else
595 delete c[conf][sid][opt];
596 }
597
598 /* only delete existing options */
599 if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) {
600 if (!d[conf])
601 d[conf] = { };
602
603 if (!d[conf][sid])
604 d[conf][sid] = { };
605
606 if (d[conf][sid] !== true)
607 d[conf][sid][opt] = true;
608 }
609 }
610 },
611
612 /**
613 * Remove the given option within the specified section of the given
614 * configuration.
615 *
616 * This function is a convenience wrapper around
617 * `uci.set(config, section, option, null)`.
618 *
619 * @param {string} conf
620 * The name of the configuration to remove the option from.
621 *
622 * @param {string} sid
623 * The name or ID of the section to remove the option from.
624 *
625 * @param {string} opt
626 * The name of the option to remove.
627 */
628 unset: function(conf, sid, opt) {
629 return this.set(conf, sid, opt, null);
630 },
631
632 /**
633 * Gets the value of the given option or the entire section object of
634 * the first found section of the specified type or the first found
635 * section of the entire configuration if no type is specified.
636 *
637 * @param {string} conf
638 * The name of the configuration to read the value from.
639 *
640 * @param {string} [type]
641 * The type of the first section to find. If it is `null`, the first
642 * section of the entire config is read, otherwise the first section
643 * matching the given type.
644 *
645 * @param {string} [opt]
646 * The option name to read the value from. If the option name is
647 * omitted or `null`, the entire section is returned instead.
648 *
649 * @returns {null|string|string[]|LuCI.uci.SectionObject}
650 * - Returns a string containing the option value in case of a
651 * plain UCI option.
652 * - Returns an array of strings containing the option values in
653 * case of `option` pointing to an UCI list.
654 * - Returns a {@link LuCI.uci.SectionObject section object} if
655 * the `option` argument has been omitted or is `null`.
656 * - Returns `null` if the config, section or option has not been
657 * found or if the corresponding configuration is not loaded.
658 */
659 get_first: function(conf, type, opt) {
660 var sid = null;
661
662 this.sections(conf, type, function(s) {
663 if (sid == null)
664 sid = s['.name'];
665 });
666
667 return this.get(conf, sid, opt);
668 },
669
670 /**
671 * Sets the value of the given option within the first found section
672 * of the given configuration matching the specified type or within
673 * the first section of the entire config when no type has is specified.
674 *
675 * If either config, type or option is null, or if `option` begins
676 * with a dot, the function will do nothing.
677 *
678 * @param {string} conf
679 * The name of the configuration to set the option value in.
680 *
681 * @param {string} [type]
682 * The type of the first section to find. If it is `null`, the first
683 * section of the entire config is written to, otherwise the first
684 * section matching the given type is used.
685 *
686 * @param {string} opt
687 * The option name to set the value for.
688 *
689 * @param {null|string|string[]} val
690 * The option value to set. If the value is `null` or an empty string,
691 * the option will be removed, otherwise it will be set or overwritten
692 * with the given value.
693 */
694 set_first: function(conf, type, opt, val) {
695 var sid = null;
696
697 this.sections(conf, type, function(s) {
698 if (sid == null)
699 sid = s['.name'];
700 });
701
702 return this.set(conf, sid, opt, val);
703 },
704
705 /**
706 * Removes the given option within the first found section of the given
707 * configuration matching the specified type or within the first section
708 * of the entire config when no type has is specified.
709 *
710 * This function is a convenience wrapper around
711 * `uci.set_first(config, type, option, null)`.
712 *
713 * @param {string} conf
714 * The name of the configuration to set the option value in.
715 *
716 * @param {string} [type]
717 * The type of the first section to find. If it is `null`, the first
718 * section of the entire config is written to, otherwise the first
719 * section matching the given type is used.
720 *
721 * @param {string} opt
722 * The option name to set the value for.
723 */
724 unset_first: function(conf, type, opt) {
725 return this.set_first(conf, type, opt, null);
726 },
727
728 /**
729 * Move the first specified section within the given configuration
730 * before or after the second specified section.
731 *
732 * @param {string} conf
733 * The configuration to move the section within.
734 *
735 * @param {string} sid1
736 * The ID of the section to move within the configuration.
737 *
738 * @param {string} [sid2]
739 * The ID of the target section for the move operation. If the
740 * `after` argument is `false` or not specified, the section named by
741 * `sid1` will be moved before this target section, if the `after`
742 * argument is `true`, the `sid1` section will be moved after this
743 * section.
744 *
745 * When the `sid2` argument is `null`, the section specified by `sid1`
746 * is moved to the end of the configuration.
747 *
748 * @param {boolean} [after=false]
749 * When `true`, the section `sid1` is moved after the section `sid2`,
750 * when `false`, the section `sid1` is moved before `sid2`.
751 *
752 * If `sid2` is null, then this parameter has no effect and the section
753 * `sid1` is moved to the end of the configuration instead.
754 *
755 * @returns {boolean}
756 * Returns `true` when the section was successfully moved, or `false`
757 * when either the section specified by `sid1` or by `sid2` is not found.
758 */
759 move: function(conf, sid1, sid2, after) {
760 var sa = this.sections(conf),
761 s1 = null, s2 = null;
762
763 sid1 = this.resolveSID(conf, sid1);
764 sid2 = this.resolveSID(conf, sid2);
765
766 for (var i = 0; i < sa.length; i++) {
767 if (sa[i]['.name'] != sid1)
768 continue;
769
770 s1 = sa[i];
771 sa.splice(i, 1);
772 break;
773 }
774
775 if (s1 == null)
776 return false;
777
778 if (sid2 == null) {
779 sa.push(s1);
780 }
781 else {
782 for (var i = 0; i < sa.length; i++) {
783 if (sa[i]['.name'] != sid2)
784 continue;
785
786 s2 = sa[i];
787 sa.splice(i + !!after, 0, s1);
788 break;
789 }
790
791 if (s2 == null)
792 return false;
793 }
794
795 for (var i = 0; i < sa.length; i++)
796 this.get(conf, sa[i]['.name'])['.index'] = i;
797
798 this.state.reorder[conf] = true;
799
800 return true;
801 },
802
803 /**
804 * Submits all local configuration changes to the remove `ubus` api,
805 * adds, removes and reorders remote sections as needed and reloads
806 * all loaded configurations to resynchronize the local state with
807 * the remote configuration values.
808 *
809 * @returns {string[]}
810 * Returns a promise resolving to an array of configuration names which
811 * have been reloaded by the save operation.
812 */
813 save: function() {
814 var v = this.state.values,
815 n = this.state.creates,
816 c = this.state.changes,
817 d = this.state.deletes,
818 r = this.state.reorder,
819 self = this,
820 snew = [ ],
821 pkgs = { },
822 tasks = [];
823
824 if (d)
825 for (var conf in d) {
826 for (var sid in d[conf]) {
827 var o = d[conf][sid];
828
829 if (o === true)
830 tasks.push(self.callDelete(conf, sid, null));
831 else
832 tasks.push(self.callDelete(conf, sid, Object.keys(o)));
833 }
834
835 pkgs[conf] = true;
836 }
837
838 if (n)
839 for (var conf in n) {
840 for (var sid in n[conf]) {
841 var p = {
842 config: conf,
843 values: { }
844 };
845
846 for (var k in n[conf][sid]) {
847 if (k == '.type')
848 p.type = n[conf][sid][k];
849 else if (k == '.create')
850 p.name = n[conf][sid][k];
851 else if (k.charAt(0) != '.')
852 p.values[k] = n[conf][sid][k];
853 }
854
855 snew.push(n[conf][sid]);
856 tasks.push(self.callAdd(p.config, p.type, p.name, p.values));
857 }
858
859 pkgs[conf] = true;
860 }
861
862 if (c)
863 for (var conf in c) {
864 for (var sid in c[conf])
865 tasks.push(self.callSet(conf, sid, c[conf][sid]));
866
867 pkgs[conf] = true;
868 }
869
870 if (r)
871 for (var conf in r)
872 pkgs[conf] = true;
873
874 return Promise.all(tasks).then(function(responses) {
875 /*
876 array "snew" holds references to the created uci sections,
877 use it to assign the returned names of the new sections
878 */
879 for (var i = 0; i < snew.length; i++)
880 snew[i]['.name'] = responses[i];
881
882 return self.reorderSections();
883 }).then(function() {
884 pkgs = Object.keys(pkgs);
885
886 self.unload(pkgs);
887
888 return self.load(pkgs);
889 });
890 },
891
892 /**
893 * Instructs the remote `ubus` UCI api to commit all saved changes with
894 * rollback protection and attempts to confirm the pending commit
895 * operation to cancel the rollback timer.
896 *
897 * @param {number} [timeout=10]
898 * Override the confirmation timeout after which a rollback is triggered.
899 *
900 * @returns {Promise<number>}
901 * Returns a promise resolving/rejecting with the `ubus` RPC status code.
902 */
903 apply: function(timeout) {
904 var self = this,
905 date = new Date();
906
907 if (typeof(timeout) != 'number' || timeout < 1)
908 timeout = 10;
909
910 return self.callApply(timeout, true).then(function(rv) {
911 if (rv != 0)
912 return Promise.reject(rv);
913
914 var try_deadline = date.getTime() + 1000 * timeout;
915 var try_confirm = function() {
916 return self.callConfirm().then(function(rv) {
917 if (rv != 0) {
918 if (date.getTime() < try_deadline)
919 window.setTimeout(try_confirm, 250);
920 else
921 return Promise.reject(rv);
922 }
923
924 return rv;
925 });
926 };
927
928 window.setTimeout(try_confirm, 1000);
929 });
930 },
931
932 /**
933 * An UCI change record is a plain array containing the change operation
934 * name as first element, the affected section ID as second argument
935 * and an optional third and fourth argument whose meanings depend on
936 * the operation.
937 *
938 * @typedef {string[]} ChangeRecord
939 * @memberof LuCI.uci
940 *
941 * @property {string} 0
942 * The operation name - may be one of `add`, `set`, `remove`, `order`,
943 * `list-add`, `list-del` or `rename`.
944 *
945 * @property {string} 1
946 * The section ID targeted by the operation.
947 *
948 * @property {string} 2
949 * The meaning of the third element depends on the operation.
950 * - For `add` it is type of the section that has been added
951 * - For `set` it either is the option name if a fourth element exists,
952 * or the type of a named section which has been added when the change
953 * entry only contains three elements.
954 * - For `remove` it contains the name of the option that has been
955 * removed.
956 * - For `order` it specifies the new sort index of the section.
957 * - For `list-add` it contains the name of the list option a new value
958 * has been added to.
959 * - For `list-del` it contains the name of the list option a value has
960 * been removed from.
961 * - For `rename` it contains the name of the option that has been
962 * renamed if a fourth element exists, else it contains the new name
963 * a section has been renamed to if the change entry only contains
964 * three elements.
965 *
966 * @property {string} 4
967 * The meaning of the fourth element depends on the operation.
968 * - For `set` it is the value an option has been set to.
969 * - For `list-add` it is the new value that has been added to a
970 * list option.
971 * - For `rename` it is the new name of an option that has been
972 * renamed.
973 */
974
975 /**
976 * Fetches uncommitted UCI changes from the remote `ubus` RPC api.
977 *
978 * @method
979 * @returns {Promise<Object<string, Array<LuCI.uci.ChangeRecord>>>}
980 * Returns a promise resolving to an object containing the configuration
981 * names as keys and arrays of related change records as values.
982 */
983 changes: rpc.declare({
984 object: 'uci',
985 method: 'changes',
986 expect: { changes: { } }
987 })
988 });