5 function isEmpty(object
, ignore
) {
6 for (var property
in object
)
7 if (object
.hasOwnProperty(property
) && property
!= ignore
)
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.
24 return baseclass
.extend(/** @lends LuCI.uci.prototype */ {
25 __init__: function() {
38 callLoad
: rpc
.declare({
42 expect
: { values
: { } },
46 callOrder
: rpc
.declare({
49 params
: [ 'config', 'sections' ],
53 callAdd
: rpc
.declare({
56 params
: [ 'config', 'type', 'name', 'values' ],
57 expect
: { section
: '' },
61 callSet
: rpc
.declare({
64 params
: [ 'config', 'section', 'values' ],
68 callDelete
: rpc
.declare({
71 params
: [ 'config', 'section', 'options' ],
75 callApply
: rpc
.declare({
78 params
: [ 'timeout', 'rollback' ],
82 callConfirm
: rpc
.declare({
90 * Generates a new, unique section ID for the given configuration.
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.
96 * @param {string} conf
97 * The configuration to generate the new section ID for.
100 * A newly generated, unique section ID in the form `newXXXXXX`
101 * where `X` denotes a hexadecimal digit.
103 createSID: function(conf
) {
104 var v
= this.state
.values
,
105 n
= this.state
.creates
,
109 sid
= "new%06x".format(Math
.random() * 0xFFFFFF);
110 } while ((n
[conf
] && n
[conf
][sid
]) || (v
[conf
] && v
[conf
][sid
]));
116 * Resolves a given section ID in extended notation to the internal
119 * @param {string} conf
120 * The configuration to resolve the section ID for.
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.
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.
134 resolveSID: function(conf
, sid
) {
135 if (typeof(sid
) != 'string')
138 var m
= /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid
);
143 sections
= this.sections(conf
, type
),
144 section
= sections
[pos
>= 0 ? pos
: sections
.length
+ pos
];
146 return section
? section
['.name'] : null;
153 reorderSections: function() {
154 var v
= this.state
.values
,
155 n
= this.state
.creates
,
156 r
= this.state
.reorder
,
159 if (Object
.keys(r
).length
=== 0)
160 return Promise
.resolve();
163 gather all created and existing sections, sort them according
164 to their index value and issue an uci order call
177 o
.sort(function(a
, b
) {
178 return (a
['.index'] - b
['.index']);
183 for (var i
= 0; i
< o
.length
; i
++)
184 sids
.push(o
[i
]['.name']);
186 tasks
.push(this.callOrder(c
, sids
));
190 this.state
.reorder
= { };
191 return Promise
.all(tasks
);
195 loadPackage: function(packageName
) {
196 if (this.loaded
[packageName
] == null)
197 return (this.loaded
[packageName
] = this.callLoad(packageName
));
199 return Promise
.resolve(this.loaded
[packageName
]);
203 * Loads the given UCI configurations from the remote `ubus` api.
205 * Loaded configurations are cached and only loaded once. Subsequent
206 * load operations of the same configurations will return the cached
209 * To force reloading a configuration, it has to be unloaded with
210 * {@link LuCI.uci#unload uci.unload()} first.
212 * @param {string|string[]} packages
213 * The name of the configuration or an array of configuration
216 * @returns {Promise<string[]>}
217 * Returns a promise resolving to the names of the configurations
218 * that have been successfully loaded.
220 load: function(packages
) {
225 if (!Array
.isArray(packages
))
226 packages
= [ packages
];
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
]));
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
];
238 if (responses
.length
)
239 document
.dispatchEvent(new CustomEvent('uci-loaded'));
246 * Unloads the given UCI configurations from the local cache.
248 * @param {string|string[]} packages
249 * The name of the configuration or an array of configuration
252 unload: function(packages
) {
253 if (!Array
.isArray(packages
))
254 packages
= [ packages
];
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
]];
262 delete this.loaded
[packages
[i
]];
267 * Adds a new section of the given type to the given configuration,
268 * optionally named according to the given name.
270 * @param {string} conf
271 * The name of the configuration to add the section to.
273 * @param {string} type
274 * The type of the section to add.
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.
281 * Returns the section ID of the newly added section which is equivalent
282 * to the given name for non-anonymous sections.
284 add: function(conf
, type
, name
) {
285 var n
= this.state
.creates
,
286 sid
= name
|| this.createSID(conf
);
296 '.index': 1000 + this.state
.newidx
++
303 * Removes the section with the given ID from the given configuration.
305 * @param {string} conf
306 * The name of the configuration to remove the section from.
308 * @param {string} sid
309 * The ID of the section to remove.
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
;
317 /* requested deletion of a just created section */
318 if (n
[conf
] && n
[conf
][sid
]) {
321 else if (v
[conf
] && v
[conf
][sid
]) {
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.
337 * Any internal metadata fields are prefixed with a dot which isn't
338 * an allowed character for normal option names.
340 * @typedef {Object<string, boolean|number|string|string[]>} SectionObject
343 * @property {boolean} .anonymous
344 * The `.anonymous` property specifies whether the configuration is
345 * anonymous (`true`) or named (`false`).
347 * @property {number} .index
348 * The `.index` property specifies the sort order of the section.
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.
355 * @property {string} .type
356 * The `.type` property contains the type of the corresponding uci
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.
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.
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.
373 * @callback LuCI.uci~sectionsFn
375 * @param {LuCI.uci.SectionObject} section
376 * The section object.
378 * @param {string} sid
379 * The name or ID of the section.
383 * Enumerates the sections of the given configuration, optionally
386 * @param {string} conf
387 * The name of the configuration to enumerate the sections for.
389 * @param {string} [type]
390 * Enumerate only sections of the given type. If omitted, enumerate
393 * @param {LuCI.uci~sectionsFn} [cb]
394 * An optional callback to invoke for each enumerated section.
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.
400 sections: function(conf
, type
, cb
) {
402 v
= this.state
.values
[conf
],
403 n
= this.state
.creates
[conf
],
404 c
= this.state
.changes
[conf
],
405 d
= this.state
.deletes
[conf
];
411 if (!d
|| d
[s
] !== true)
412 if (!type
|| v
[s
]['.type'] == type
)
413 sa
.push(Object
.assign({ }, v
[s
], c
? c
[s
] : null));
417 if (!type
|| n
[s
]['.type'] == type
)
418 sa
.push(Object
.assign({ }, n
[s
]));
420 sa
.sort(function(a
, b
) {
421 return a
['.index'] - b
['.index'];
424 for (var i
= 0; i
< sa
.length
; i
++)
427 if (typeof(cb
) == 'function')
428 for (var i
= 0; i
< sa
.length
; i
++)
429 cb
.call(this, sa
[i
], sa
[i
]['.name']);
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.
439 * @param {string} conf
440 * The name of the configuration to read the value from.
442 * @param {string} sid
443 * The name or ID of the section to read.
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.
449 * @returns {null|string|string[]|LuCI.uci.SectionObject}
450 * - Returns a string containing the option value in case of a
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.
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
;
465 sid
= this.resolveSID(conf
, sid
);
470 /* requested option in a just created section */
471 if (n
[conf
] && n
[conf
][sid
]) {
478 return n
[conf
][sid
][opt
];
481 /* requested an option value */
483 /* check whether option was deleted */
484 if (d
[conf
] && d
[conf
][sid
])
485 if (d
[conf
][sid
] === true || d
[conf
][sid
][opt
])
488 /* check whether option was changed */
489 if (c
[conf
] && c
[conf
][sid
] && c
[conf
][sid
][opt
] != null)
490 return c
[conf
][sid
][opt
];
492 /* return base value */
493 if (v
[conf
] && v
[conf
][sid
])
494 return v
[conf
][sid
][opt
];
499 /* requested an entire section */
501 /* check whether entire section was deleted */
502 if (d
[conf
] && d
[conf
][sid
] === true)
505 var s
= v
[conf
][sid
] || null;
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
];
514 /* merge deletions */
515 if (d
[conf
] && d
[conf
][sid
])
516 for (var opt
in d
[conf
][sid
])
527 * Sets the value of the given option within the specified section
528 * of the given configuration.
530 * If either config, section or option is null, or if `option` begins
531 * with a dot, the function will do nothing.
533 * @param {string} conf
534 * The name of the configuration to set the option value in.
536 * @param {string} sid
537 * The name or ID of the section to set the option value in.
539 * @param {string} opt
540 * The option name to set the value for.
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.
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
;
553 sid
= this.resolveSID(conf
, sid
);
555 if (sid
== null || opt
== null || opt
.charAt(0) == '.')
558 if (n
[conf
] && n
[conf
][sid
]) {
560 n
[conf
][sid
][opt
] = val
;
562 delete n
[conf
][sid
][opt
];
564 else if (val
!= null && val
!== '') {
565 /* do not set within deleted section */
566 if (d
[conf
] && d
[conf
][sid
] === true)
569 /* only set in existing sections */
570 if (!v
[conf
] || !v
[conf
][sid
])
579 /* undelete option */
580 if (d
[conf
] && d
[conf
][sid
]) {
581 if (isEmpty(d
[conf
][sid
], opt
))
584 delete d
[conf
][sid
][opt
];
587 c
[conf
][sid
][opt
] = val
;
590 /* revert any change for to-be-deleted option */
591 if (c
[conf
] && c
[conf
][sid
]) {
592 if (isEmpty(c
[conf
][sid
], opt
))
595 delete c
[conf
][sid
][opt
];
598 /* only delete existing options */
599 if (v
[conf
] && v
[conf
][sid
] && v
[conf
][sid
].hasOwnProperty(opt
)) {
606 if (d
[conf
][sid
] !== true)
607 d
[conf
][sid
][opt
] = true;
613 * Remove the given option within the specified section of the given
616 * This function is a convenience wrapper around
617 * `uci.set(config, section, option, null)`.
619 * @param {string} conf
620 * The name of the configuration to remove the option from.
622 * @param {string} sid
623 * The name or ID of the section to remove the option from.
625 * @param {string} opt
626 * The name of the option to remove.
628 unset: function(conf
, sid
, opt
) {
629 return this.set(conf
, sid
, opt
, null);
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.
637 * @param {string} conf
638 * The name of the configuration to read the value from.
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.
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.
649 * @returns {null|string|string[]|LuCI.uci.SectionObject}
650 * - Returns a string containing the option value in case of a
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.
659 get_first: function(conf
, type
, opt
) {
662 this.sections(conf
, type
, function(s
) {
667 return this.get(conf
, sid
, opt
);
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.
675 * If either config, type or option is null, or if `option` begins
676 * with a dot, the function will do nothing.
678 * @param {string} conf
679 * The name of the configuration to set the option value in.
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.
686 * @param {string} opt
687 * The option name to set the value for.
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.
694 set_first: function(conf
, type
, opt
, val
) {
697 this.sections(conf
, type
, function(s
) {
702 return this.set(conf
, sid
, opt
, val
);
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.
710 * This function is a convenience wrapper around
711 * `uci.set_first(config, type, option, null)`.
713 * @param {string} conf
714 * The name of the configuration to set the option value in.
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.
721 * @param {string} opt
722 * The option name to set the value for.
724 unset_first: function(conf
, type
, opt
) {
725 return this.set_first(conf
, type
, opt
, null);
729 * Move the first specified section within the given configuration
730 * before or after the second specified section.
732 * @param {string} conf
733 * The configuration to move the section within.
735 * @param {string} sid1
736 * The ID of the section to move within the configuration.
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
745 * When the `sid2` argument is `null`, the section specified by `sid1`
746 * is moved to the end of the configuration.
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`.
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.
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.
759 move: function(conf
, sid1
, sid2
, after
) {
760 var sa
= this.sections(conf
),
761 s1
= null, s2
= null;
763 sid1
= this.resolveSID(conf
, sid1
);
764 sid2
= this.resolveSID(conf
, sid2
);
766 for (var i
= 0; i
< sa
.length
; i
++) {
767 if (sa
[i
]['.name'] != sid1
)
782 for (var i
= 0; i
< sa
.length
; i
++) {
783 if (sa
[i
]['.name'] != sid2
)
787 sa
.splice(i
+ !!after
, 0, s1
);
795 for (var i
= 0; i
< sa
.length
; i
++)
796 this.get(conf
, sa
[i
]['.name'])['.index'] = i
;
798 this.state
.reorder
[conf
] = true;
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.
809 * @returns {string[]}
810 * Returns a promise resolving to an array of configuration names which
811 * have been reloaded by the save operation.
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
,
825 for (var conf
in d
) {
826 for (var sid
in d
[conf
]) {
827 var o
= d
[conf
][sid
];
830 tasks
.push(self
.callDelete(conf
, sid
, null));
832 tasks
.push(self
.callDelete(conf
, sid
, Object
.keys(o
)));
839 for (var conf
in n
) {
840 for (var sid
in n
[conf
]) {
846 for (var k
in n
[conf
][sid
]) {
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
];
855 snew
.push(n
[conf
][sid
]);
856 tasks
.push(self
.callAdd(p
.config
, p
.type
, p
.name
, p
.values
));
863 for (var conf
in c
) {
864 for (var sid
in c
[conf
])
865 tasks
.push(self
.callSet(conf
, sid
, c
[conf
][sid
]));
874 return Promise
.all(tasks
).then(function(responses
) {
876 array "snew" holds references to the created uci sections,
877 use it to assign the returned names of the new sections
879 for (var i
= 0; i
< snew
.length
; i
++)
880 snew
[i
]['.name'] = responses
[i
];
882 return self
.reorderSections();
884 pkgs
= Object
.keys(pkgs
);
888 return self
.load(pkgs
);
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.
897 * @param {number} [timeout=10]
898 * Override the confirmation timeout after which a rollback is triggered.
900 * @returns {Promise<number>}
901 * Returns a promise resolving/rejecting with the `ubus` RPC status code.
903 apply: function(timeout
) {
907 if (typeof(timeout
) != 'number' || timeout
< 1)
910 return self
.callApply(timeout
, true).then(function(rv
) {
912 return Promise
.reject(rv
);
914 var try_deadline
= date
.getTime() + 1000 * timeout
;
915 var try_confirm = function() {
916 return self
.callConfirm().then(function(rv
) {
918 if (date
.getTime() < try_deadline
)
919 window
.setTimeout(try_confirm
, 250);
921 return Promise
.reject(rv
);
928 window
.setTimeout(try_confirm
, 1000);
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
938 * @typedef {string[]} ChangeRecord
941 * @property {string} 0
942 * The operation name - may be one of `add`, `set`, `remove`, `order`,
943 * `list-add`, `list-del` or `rename`.
945 * @property {string} 1
946 * The section ID targeted by the operation.
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
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
959 * - For `list-del` it contains the name of the list option a value has
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
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
971 * - For `rename` it is the new name of an option that has been
976 * Fetches uncommitted UCI changes from the remote `ubus` RPC api.
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.
983 changes
: rpc
.declare({
986 expect
: { changes
: { } }