2
0

uci.js 26 KB

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