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