luci-base: luci.js: support registering request progress handlers
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / uci.js
1 'use strict';
2 'require rpc';
3
4 return L.Class.extend({
5         __init__: function() {
6                 this.state = {
7                         newidx:  0,
8                         values:  { },
9                         creates: { },
10                         changes: { },
11                         deletes: { },
12                         reorder: { }
13                 };
14
15                 this.loaded = {};
16         },
17
18         callLoad: rpc.declare({
19                 object: 'uci',
20                 method: 'get',
21                 params: [ 'config' ],
22                 expect: { values: { } }
23         }),
24
25         callOrder: rpc.declare({
26                 object: 'uci',
27                 method: 'order',
28                 params: [ 'config', 'sections' ]
29         }),
30
31         callAdd: rpc.declare({
32                 object: 'uci',
33                 method: 'add',
34                 params: [ 'config', 'type', 'name', 'values' ],
35                 expect: { section: '' }
36         }),
37
38         callSet: rpc.declare({
39                 object: 'uci',
40                 method: 'set',
41                 params: [ 'config', 'section', 'values' ]
42         }),
43
44         callDelete: rpc.declare({
45                 object: 'uci',
46                 method: 'delete',
47                 params: [ 'config', 'section', 'options' ]
48         }),
49
50         callApply: rpc.declare({
51                 object: 'uci',
52                 method: 'apply',
53                 params: [ 'timeout', 'rollback' ]
54         }),
55
56         callConfirm: rpc.declare({
57                 object: 'uci',
58                 method: 'confirm'
59         }),
60
61         createSID: function(conf) {
62                 var v = this.state.values,
63                     n = this.state.creates,
64                     sid;
65
66                 do {
67                         sid = "new%06x".format(Math.random() * 0xFFFFFF);
68                 } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
69
70                 return sid;
71         },
72
73         resolveSID: function(conf, sid) {
74                 if (typeof(sid) != 'string')
75                         return sid;
76
77                 var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
78
79                 if (m) {
80                         var type = m[1],
81                             pos = +m[2],
82                             sections = this.sections(conf, type),
83                             section = sections[pos >= 0 ? pos : sections.length + pos];
84
85                         return section ? section['.name'] : null;
86                 }
87
88                 return sid;
89         },
90
91         reorderSections: function() {
92                 var v = this.state.values,
93                     n = this.state.creates,
94                     r = this.state.reorder,
95                     tasks = [];
96
97                 if (Object.keys(r).length === 0)
98                         return Promise.resolve();
99
100                 /*
101                  gather all created and existing sections, sort them according
102                  to their index value and issue an uci order call
103                 */
104                 for (var c in r) {
105                         var o = [ ];
106
107                         if (n[c])
108                                 for (var s in n[c])
109                                         o.push(n[c][s]);
110
111                         for (var s in v[c])
112                                 o.push(v[c][s]);
113
114                         if (o.length > 0) {
115                                 o.sort(function(a, b) {
116                                         return (a['.index'] - b['.index']);
117                                 });
118
119                                 var sids = [ ];
120
121                                 for (var i = 0; i < o.length; i++)
122                                         sids.push(o[i]['.name']);
123
124                                 tasks.push(this.callOrder(c, sids));
125                         }
126                 }
127
128                 this.state.reorder = { };
129                 return Promise.all(tasks);
130         },
131
132         loadPackage: function(packageName) {
133                 if (this.loaded[packageName] == null)
134                         return (this.loaded[packageName] = this.callLoad(packageName));
135
136                 return Promise.resolve(this.loaded[packageName]);
137         },
138
139         load: function(packages) {
140                 var self = this,
141                     pkgs = [ ],
142                     tasks = [];
143
144                 if (!Array.isArray(packages))
145                         packages = [ packages ];
146
147                 for (var i = 0; i < packages.length; i++)
148                         if (!self.state.values[packages[i]]) {
149                                 pkgs.push(packages[i]);
150                                 tasks.push(self.loadPackage(packages[i]));
151                         }
152
153                 return Promise.all(tasks).then(function(responses) {
154                         for (var i = 0; i < responses.length; i++)
155                                 self.state.values[pkgs[i]] = responses[i];
156
157                         if (responses.length)
158                                 document.dispatchEvent(new CustomEvent('uci-loaded'));
159
160                         return pkgs;
161                 });
162         },
163
164         unload: function(packages) {
165                 if (!Array.isArray(packages))
166                         packages = [ packages ];
167
168                 for (var i = 0; i < packages.length; i++) {
169                         delete this.state.values[packages[i]];
170                         delete this.state.creates[packages[i]];
171                         delete this.state.changes[packages[i]];
172                         delete this.state.deletes[packages[i]];
173
174                         delete this.loaded[packages[i]];
175                 }
176         },
177
178         add: function(conf, type, name) {
179                 var n = this.state.creates,
180                     sid = name || this.createSID(conf);
181
182                 if (!n[conf])
183                         n[conf] = { };
184
185                 n[conf][sid] = {
186                         '.type':      type,
187                         '.name':      sid,
188                         '.create':    name,
189                         '.anonymous': !name,
190                         '.index':     1000 + this.state.newidx++
191                 };
192
193                 return sid;
194         },
195
196         remove: function(conf, sid) {
197                 var n = this.state.creates,
198                     c = this.state.changes,
199                     d = this.state.deletes;
200
201                 /* requested deletion of a just created section */
202                 if (n[conf] && n[conf][sid]) {
203                         delete n[conf][sid];
204                 }
205                 else {
206                         if (c[conf])
207                                 delete c[conf][sid];
208
209                         if (!d[conf])
210                                 d[conf] = { };
211
212                         d[conf][sid] = true;
213                 }
214         },
215
216         sections: function(conf, type, cb) {
217                 var sa = [ ],
218                     v = this.state.values[conf],
219                     n = this.state.creates[conf],
220                     c = this.state.changes[conf],
221                     d = this.state.deletes[conf];
222
223                 if (!v)
224                         return sa;
225
226                 for (var s in v)
227                         if (!d || d[s] !== true)
228                                 if (!type || v[s]['.type'] == type)
229                                         sa.push(Object.assign({ }, v[s], c ? c[s] : undefined));
230
231                 if (n)
232                         for (var s in n)
233                                 if (!type || n[s]['.type'] == type)
234                                         sa.push(Object.assign({ }, n[s]));
235
236                 sa.sort(function(a, b) {
237                         return a['.index'] - b['.index'];
238                 });
239
240                 for (var i = 0; i < sa.length; i++)
241                         sa[i]['.index'] = i;
242
243                 if (typeof(cb) == 'function')
244                         for (var i = 0; i < sa.length; i++)
245                                 cb.call(this, sa[i], sa[i]['.name']);
246
247                 return sa;
248         },
249
250         get: function(conf, sid, opt) {
251                 var v = this.state.values,
252                     n = this.state.creates,
253                     c = this.state.changes,
254                     d = this.state.deletes;
255
256                 sid = this.resolveSID(conf, sid);
257
258                 if (sid == null)
259                         return null;
260
261                 /* requested option in a just created section */
262                 if (n[conf] && n[conf][sid]) {
263                         if (!n[conf])
264                                 return undefined;
265
266                         if (opt == null)
267                                 return n[conf][sid];
268
269                         return n[conf][sid][opt];
270                 }
271
272                 /* requested an option value */
273                 if (opt != null) {
274                         /* check whether option was deleted */
275                         if (d[conf] && d[conf][sid]) {
276                                 if (d[conf][sid] === true)
277                                         return undefined;
278
279                                 for (var i = 0; i < d[conf][sid].length; i++)
280                                         if (d[conf][sid][i] == opt)
281                                                 return undefined;
282                         }
283
284                         /* check whether option was changed */
285                         if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
286                                 return c[conf][sid][opt];
287
288                         /* return base value */
289                         if (v[conf] && v[conf][sid])
290                                 return v[conf][sid][opt];
291
292                         return undefined;
293                 }
294
295                 /* requested an entire section */
296                 if (v[conf])
297                         return v[conf][sid];
298
299                 return undefined;
300         },
301
302         set: function(conf, sid, opt, val) {
303                 var v = this.state.values,
304                     n = this.state.creates,
305                     c = this.state.changes,
306                     d = this.state.deletes;
307
308                 sid = this.resolveSID(conf, sid);
309
310                 if (sid == null || opt == null || opt.charAt(0) == '.')
311                         return;
312
313                 if (n[conf] && n[conf][sid]) {
314                         if (val != null)
315                                 n[conf][sid][opt] = val;
316                         else
317                                 delete n[conf][sid][opt];
318                 }
319                 else if (val != null && val !== '') {
320                         /* do not set within deleted section */
321                         if (d[conf] && d[conf][sid] === true)
322                                 return;
323
324                         /* only set in existing sections */
325                         if (!v[conf] || !v[conf][sid])
326                                 return;
327
328                         if (!c[conf])
329                                 c[conf] = {};
330
331                         if (!c[conf][sid])
332                                 c[conf][sid] = {};
333
334                         /* undelete option */
335                         if (d[conf] && d[conf][sid])
336                                 d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt });
337
338                         c[conf][sid][opt] = val;
339                 }
340                 else {
341                         /* only delete in existing sections */
342                         if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) &&
343                             !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt)))
344                             return;
345
346                         if (!d[conf])
347                                 d[conf] = { };
348
349                         if (!d[conf][sid])
350                                 d[conf][sid] = [ ];
351
352                         if (d[conf][sid] !== true)
353                                 d[conf][sid].push(opt);
354                 }
355         },
356
357         unset: function(conf, sid, opt) {
358                 return this.set(conf, sid, opt, null);
359         },
360
361         get_first: function(conf, type, opt) {
362                 var sid = null;
363
364                 this.sections(conf, type, function(s) {
365                         if (sid == null)
366                                 sid = s['.name'];
367                 });
368
369                 return this.get(conf, sid, opt);
370         },
371
372         set_first: function(conf, type, opt, val) {
373                 var sid = null;
374
375                 this.sections(conf, type, function(s) {
376                         if (sid == null)
377                                 sid = s['.name'];
378                 });
379
380                 return this.set(conf, sid, opt, val);
381         },
382
383         unset_first: function(conf, type, opt) {
384                 return this.set_first(conf, type, opt, null);
385         },
386
387         move: function(conf, sid1, sid2, after) {
388                 var sa = this.sections(conf),
389                     s1 = null, s2 = null;
390
391                 sid1 = this.resolveSID(conf, sid1);
392                 sid2 = this.resolveSID(conf, sid2);
393
394                 for (var i = 0; i < sa.length; i++) {
395                         if (sa[i]['.name'] != sid1)
396                                 continue;
397
398                         s1 = sa[i];
399                         sa.splice(i, 1);
400                         break;
401                 }
402
403                 if (s1 == null)
404                         return false;
405
406                 if (sid2 == null) {
407                         sa.push(s1);
408                 }
409                 else {
410                         for (var i = 0; i < sa.length; i++) {
411                                 if (sa[i]['.name'] != sid2)
412                                         continue;
413
414                                 s2 = sa[i];
415                                 sa.splice(i + !!after, 0, s1);
416                                 break;
417                         }
418
419                         if (s2 == null)
420                                 return false;
421                 }
422
423                 for (var i = 0; i < sa.length; i++)
424                         this.get(conf, sa[i]['.name'])['.index'] = i;
425
426                 this.state.reorder[conf] = true;
427
428                 return true;
429         },
430
431         save: function() {
432                 var v = this.state.values,
433                     n = this.state.creates,
434                     c = this.state.changes,
435                     d = this.state.deletes,
436                     r = this.state.reorder,
437                     self = this,
438                     snew = [ ],
439                     pkgs = { },
440                     tasks = [];
441
442                 if (n)
443                         for (var conf in n) {
444                                 for (var sid in n[conf]) {
445                                         var r = {
446                                                 config: conf,
447                                                 values: { }
448                                         };
449
450                                         for (var k in n[conf][sid]) {
451                                                 if (k == '.type')
452                                                         r.type = n[conf][sid][k];
453                                                 else if (k == '.create')
454                                                         r.name = n[conf][sid][k];
455                                                 else if (k.charAt(0) != '.')
456                                                         r.values[k] = n[conf][sid][k];
457                                         }
458
459                                         snew.push(n[conf][sid]);
460                                         tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
461                                 }
462
463                                 pkgs[conf] = true;
464                         }
465
466                 if (c)
467                         for (var conf in c) {
468                                 for (var sid in c[conf])
469                                         tasks.push(self.callSet(conf, sid, c[conf][sid]));
470
471                                 pkgs[conf] = true;
472                         }
473
474                 if (d)
475                         for (var conf in d) {
476                                 for (var sid in d[conf]) {
477                                         var o = d[conf][sid];
478                                         tasks.push(self.callDelete(conf, sid, (o === true) ? null : o));
479                                 }
480
481                                 pkgs[conf] = true;
482                         }
483
484                 if (r)
485                         for (var conf in r)
486                                 pkgs[conf] = true;
487
488                 return Promise.all(tasks).then(function(responses) {
489                         /*
490                          array "snew" holds references to the created uci sections,
491                          use it to assign the returned names of the new sections
492                         */
493                         for (var i = 0; i < snew.length; i++)
494                                 snew[i]['.name'] = responses[i];
495
496                         return self.reorderSections();
497                 }).then(function() {
498                         pkgs = Object.keys(pkgs);
499
500                         self.unload(pkgs);
501
502                         return self.load(pkgs);
503                 });
504         },
505
506         apply: function(timeout) {
507                 var self = this,
508                     date = new Date();
509
510                 if (typeof(timeout) != 'number' || timeout < 1)
511                         timeout = 10;
512
513                 return self.callApply(timeout, true).then(function(rv) {
514                         if (rv != 0)
515                                 return Promise.reject(rv);
516
517                         var try_deadline = date.getTime() + 1000 * timeout;
518                         var try_confirm = function() {
519                                 return self.callConfirm().then(function(rv) {
520                                         if (rv != 0) {
521                                                 if (date.getTime() < try_deadline)
522                                                         window.setTimeout(try_confirm, 250);
523                                                 else
524                                                         return Promise.reject(rv);
525                                         }
526
527                                         return rv;
528                                 });
529                         };
530
531                         window.setTimeout(try_confirm, 1000);
532                 });
533         },
534
535         changes: rpc.declare({
536                 object: 'uci',
537                 method: 'changes',
538                 expect: { changes: { } }
539         })
540 });