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