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