677edf6addb1274d870b21a1b0ed0bc5ae580df8
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.
15 return L
.Class
.extend(/** @lends LuCI.uci.prototype */ {
16 __init__: function() {
29 callLoad
: rpc
.declare({
33 expect
: { values
: { } }
37 callOrder
: rpc
.declare({
40 params
: [ 'config', 'sections' ]
43 callAdd
: rpc
.declare({
46 params
: [ 'config', 'type', 'name', 'values' ],
47 expect
: { section
: '' }
50 callSet
: rpc
.declare({
53 params
: [ 'config', 'section', 'values' ]
56 callDelete
: rpc
.declare({
59 params
: [ 'config', 'section', 'options' ]
62 callApply
: rpc
.declare({
65 params
: [ 'timeout', 'rollback' ]
68 callConfirm
: rpc
.declare({
75 * Generates a new, unique section ID for the given configuration.
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.
81 * @param {string} config
82 * The configuration to generate the new section ID for.
85 * A newly generated, unique section ID in the form `newXXXXXX`
86 * where `X` denotes a hexadecimal digit.
88 createSID: function(conf
) {
89 var v
= this.state
.values
,
90 n
= this.state
.creates
,
94 sid
= "new%06x".format(Math
.random() * 0xFFFFFF);
95 } while ((n
[conf
] && n
[conf
][sid
]) || (v
[conf
] && v
[conf
][sid
]));
101 * Resolves a given section ID in extended notation to the internal
104 * @param {string} config
105 * The configuration to resolve the section ID for.
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.
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.
119 resolveSID: function(conf
, sid
) {
120 if (typeof(sid
) != 'string')
123 var m
= /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid
);
128 sections
= this.sections(conf
, type
),
129 section
= sections
[pos
>= 0 ? pos
: sections
.length
+ pos
];
131 return section
? section
['.name'] : null;
138 reorderSections: function() {
139 var v
= this.state
.values
,
140 n
= this.state
.creates
,
141 r
= this.state
.reorder
,
144 if (Object
.keys(r
).length
=== 0)
145 return Promise
.resolve();
148 gather all created and existing sections, sort them according
149 to their index value and issue an uci order call
162 o
.sort(function(a
, b
) {
163 return (a
['.index'] - b
['.index']);
168 for (var i
= 0; i
< o
.length
; i
++)
169 sids
.push(o
[i
]['.name']);
171 tasks
.push(this.callOrder(c
, sids
));
175 this.state
.reorder
= { };
176 return Promise
.all(tasks
);
180 loadPackage: function(packageName
) {
181 if (this.loaded
[packageName
] == null)
182 return (this.loaded
[packageName
] = this.callLoad(packageName
));
184 return Promise
.resolve(this.loaded
[packageName
]);
188 * Loads the given UCI configurations from the remote `ubus` api.
190 * Loaded configurations are cached and only loaded once. Subsequent
191 * load operations of the same configurations will return the cached
194 * To force reloading a configuration, it has to be unloaded with
195 * {@link LuCI.uci#unload uci.unload()} first.
197 * @param {string|string[]} config
198 * The name of the configuration or an array of configuration
201 * @returns {Promise<string[]>}
202 * Returns a promise resolving to the names of the configurations
203 * that have been successfully loaded.
205 load: function(packages
) {
210 if (!Array
.isArray(packages
))
211 packages
= [ packages
];
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
]));
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
];
223 if (responses
.length
)
224 document
.dispatchEvent(new CustomEvent('uci-loaded'));
231 * Unloads the given UCI configurations from the local cache.
233 * @param {string|string[]} config
234 * The name of the configuration or an array of configuration
237 unload: function(packages
) {
238 if (!Array
.isArray(packages
))
239 packages
= [ packages
];
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
]];
247 delete this.loaded
[packages
[i
]];
252 * Adds a new section of the given type to the given configuration,
253 * optionally named according to the given name.
255 * @param {string} config
256 * The name of the configuration to add the section to.
258 * @param {string} type
259 * The type of the section to add.
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.
266 * Returns the section ID of the newly added section which is equivalent
267 * to the given name for non-anonymous sections.
269 add: function(conf
, type
, name
) {
270 var n
= this.state
.creates
,
271 sid
= name
|| this.createSID(conf
);
281 '.index': 1000 + this.state
.newidx
++
288 * Removes the section with the given ID from the given configuration.
290 * @param {string} config
291 * The name of the configuration to remove the section from.
293 * @param {string} sid
294 * The ID of the section to remove.
296 remove: function(conf
, sid
) {
297 var n
= this.state
.creates
,
298 c
= this.state
.changes
,
299 d
= this.state
.deletes
;
301 /* requested deletion of a just created section */
302 if (n
[conf
] && n
[conf
][sid
]) {
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.
321 * Any internal metadata fields are prefixed with a dot which is isn't
322 * an allowed character for normal option names.
324 * @typedef {Object<string, boolean|number|string|string[]>} SectionObject
327 * @property {boolean} .anonymous
328 * The `.anonymous` property specifies whether the configuration is
329 * anonymous (`true`) or named (`false`).
331 * @property {number} .index
332 * The `.index` property specifes the sort order of the section.
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.
339 * @property {string} .type
340 * The `.type` property contains the type of the corresponding uci
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.
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.
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.
357 * @callback LuCI.uci~sectionsFn
359 * @param {LuCI.uci.SectionObject} section
360 * The section object.
362 * @param {string} sid
363 * The name or ID of the section.
367 * Enumerates the sections of the given configuration, optionally
370 * @param {string} config
371 * The name of the configuration to enumerate the sections for.
373 * @param {string} [type]
374 * Enumerate only sections of the given type. If omitted, enumerate
377 * @param {LuCI.uci~sectionsFn} [cb]
378 * An optional callback to invoke for each enumerated section.
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.
384 sections: function(conf
, type
, cb
) {
386 v
= this.state
.values
[conf
],
387 n
= this.state
.creates
[conf
],
388 c
= this.state
.changes
[conf
],
389 d
= this.state
.deletes
[conf
];
395 if (!d
|| d
[s
] !== true)
396 if (!type
|| v
[s
]['.type'] == type
)
397 sa
.push(Object
.assign({ }, v
[s
], c
? c
[s
] : undefined));
401 if (!type
|| n
[s
]['.type'] == type
)
402 sa
.push(Object
.assign({ }, n
[s
]));
404 sa
.sort(function(a
, b
) {
405 return a
['.index'] - b
['.index'];
408 for (var i
= 0; i
< sa
.length
; i
++)
411 if (typeof(cb
) == 'function')
412 for (var i
= 0; i
< sa
.length
; i
++)
413 cb
.call(this, sa
[i
], sa
[i
]['.name']);
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.
423 * @param {string} config
424 * The name of the configuration to read the value from.
426 * @param {string} sid
427 * The name or ID of the section to read.
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.
433 * @returns {null|string|string[]|LuCI.uci.SectionObject}
434 * - Returns a string containing the option value in case of a
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.
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
;
449 sid
= this.resolveSID(conf
, sid
);
454 /* requested option in a just created section */
455 if (n
[conf
] && n
[conf
][sid
]) {
462 return n
[conf
][sid
][opt
];
465 /* requested an option value */
467 /* check whether option was deleted */
468 if (d
[conf
] && d
[conf
][sid
]) {
469 if (d
[conf
][sid
] === true)
472 for (var i
= 0; i
< d
[conf
][sid
].length
; i
++)
473 if (d
[conf
][sid
][i
] == opt
)
477 /* check whether option was changed */
478 if (c
[conf
] && c
[conf
][sid
] && c
[conf
][sid
][opt
] != null)
479 return c
[conf
][sid
][opt
];
481 /* return base value */
482 if (v
[conf
] && v
[conf
][sid
])
483 return v
[conf
][sid
][opt
];
488 /* requested an entire section */
496 * Sets the value of the given option within the specified section
497 * of the given configuration.
499 * If either config, section or option is null, or if `option` begins
500 * with a dot, the function will do nothing.
502 * @param {string} config
503 * The name of the configuration to set the option value in.
505 * @param {string} sid
506 * The name or ID of the section to set the option value in.
508 * @param {string} option
509 * The option name to set the value for.
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.
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
;
522 sid
= this.resolveSID(conf
, sid
);
524 if (sid
== null || opt
== null || opt
.charAt(0) == '.')
527 if (n
[conf
] && n
[conf
][sid
]) {
529 n
[conf
][sid
][opt
] = val
;
531 delete n
[conf
][sid
][opt
];
533 else if (val
!= null && val
!== '') {
534 /* do not set within deleted section */
535 if (d
[conf
] && d
[conf
][sid
] === true)
538 /* only set in existing sections */
539 if (!v
[conf
] || !v
[conf
][sid
])
548 /* undelete option */
549 if (d
[conf
] && d
[conf
][sid
])
550 d
[conf
][sid
] = d
[conf
][sid
].filter(function(o
) { return o
!== opt
});
552 c
[conf
][sid
][opt
] = val
;
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
)))
566 if (d
[conf
][sid
] !== true)
567 d
[conf
][sid
].push(opt
);
572 * Remove the given option within the specified section of the given
575 * This function is a convenience wrapper around
576 * `uci.set(config, section, option, null)`.
578 * @param {string} config
579 * The name of the configuration to remove the option from.
581 * @param {string} sid
582 * The name or ID of the section to remove the option from.
584 * @param {string} option
585 * The name of the option to remove.
587 unset: function(conf
, sid
, opt
) {
588 return this.set(conf
, sid
, opt
, null);
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.
596 * @param {string} config
597 * The name of the configuration to read the value from.
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.
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.
608 * @returns {null|string|string[]|LuCI.uci.SectionObject}
609 * - Returns a string containing the option value in case of a
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.
618 get_first: function(conf
, type
, opt
) {
621 this.sections(conf
, type
, function(s
) {
626 return this.get(conf
, sid
, opt
);
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.
634 * If either config, type or option is null, or if `option` begins
635 * with a dot, the function will do nothing.
637 * @param {string} config
638 * The name of the configuration to set the option value in.
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.
645 * @param {string} option
646 * The option name to set the value for.
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.
653 set_first: function(conf
, type
, opt
, val
) {
656 this.sections(conf
, type
, function(s
) {
661 return this.set(conf
, sid
, opt
, val
);
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.
669 * This function is a convenience wrapper around
670 * `uci.set_first(config, type, option, null)`.
672 * @param {string} config
673 * The name of the configuration to set the option value in.
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.
680 * @param {string} option
681 * The option name to set the value for.
683 unset_first: function(conf
, type
, opt
) {
684 return this.set_first(conf
, type
, opt
, null);
688 * Move the first specified section within the given configuration
689 * before or after the second specified section.
691 * @param {string} config
692 * The configuration to move the section within.
694 * @param {string} sid1
695 * The ID of the section to move within the configuration.
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
704 * When the `sid2` argument is `null`, the section specified by `sid1`
705 * is moved to the end of the configuration.
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`.
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.
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.
718 move: function(conf
, sid1
, sid2
, after
) {
719 var sa
= this.sections(conf
),
720 s1
= null, s2
= null;
722 sid1
= this.resolveSID(conf
, sid1
);
723 sid2
= this.resolveSID(conf
, sid2
);
725 for (var i
= 0; i
< sa
.length
; i
++) {
726 if (sa
[i
]['.name'] != sid1
)
741 for (var i
= 0; i
< sa
.length
; i
++) {
742 if (sa
[i
]['.name'] != sid2
)
746 sa
.splice(i
+ !!after
, 0, s1
);
754 for (var i
= 0; i
< sa
.length
; i
++)
755 this.get(conf
, sa
[i
]['.name'])['.index'] = i
;
757 this.state
.reorder
[conf
] = true;
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.
768 * @returns {string[]}
769 * Returns a promise resolving to an array of configuration names which
770 * have been reloaded by the save operation.
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
,
784 for (var conf
in n
) {
785 for (var sid
in n
[conf
]) {
791 for (var k
in n
[conf
][sid
]) {
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
];
800 snew
.push(n
[conf
][sid
]);
801 tasks
.push(self
.callAdd(r
.config
, r
.type
, r
.name
, r
.values
));
808 for (var conf
in c
) {
809 for (var sid
in c
[conf
])
810 tasks
.push(self
.callSet(conf
, sid
, c
[conf
][sid
]));
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
));
829 return Promise
.all(tasks
).then(function(responses
) {
831 array "snew" holds references to the created uci sections,
832 use it to assign the returned names of the new sections
834 for (var i
= 0; i
< snew
.length
; i
++)
835 snew
[i
]['.name'] = responses
[i
];
837 return self
.reorderSections();
839 pkgs
= Object
.keys(pkgs
);
843 return self
.load(pkgs
);
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.
852 * @param {number} [timeout=10]
853 * Override the confirmation timeout after which a rollback is triggered.
855 * @returns {Promise<number>}
856 * Returns a promise resolving/rejecting with the `ubus` RPC status code.
858 apply: function(timeout
) {
862 if (typeof(timeout
) != 'number' || timeout
< 1)
865 return self
.callApply(timeout
, true).then(function(rv
) {
867 return Promise
.reject(rv
);
869 var try_deadline
= date
.getTime() + 1000 * timeout
;
870 var try_confirm = function() {
871 return self
.callConfirm().then(function(rv
) {
873 if (date
.getTime() < try_deadline
)
874 window
.setTimeout(try_confirm
, 250);
876 return Promise
.reject(rv
);
883 window
.setTimeout(try_confirm
, 1000);
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
893 * @typedef {string[]} ChangeRecord
896 * @property {string} 0
897 * The operation name - may be one of `add`, `set`, `remove`, `order`,
898 * `list-add`, `list-del` or `rename`.
900 * @property {string} 1
901 * The section ID targeted by the operation.
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
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
914 * - For `list-del` it contains the name of the list option a value has
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
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
926 * - For `rename` it is the new name of an option that has been
931 * Fetches uncommitted UCI changes from the remote `ubus` RPC api.
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.
938 changes
: rpc
.declare({
941 expect
: { changes
: { } }