luci2: implement LuCI2.cbi.Modal widget
[project/luci2/ui.git] / luci2 / htdocs / luci2 / view / system.users.js
1 L.ui.view.extend({
2     aclTable: L.cbi.AbstractValue.extend({
3         strGlob: function(pattern, match) {
4             var re = new RegExp('^' + (pattern
5                 .replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
6                 .replace(/\\\*/g, '.*?')) + '$');
7
8             return re.test(match);
9         },
10
11         aclMatch: function(list, group) {
12             for (var i = 0; i < list.length; i++)
13             {
14                 var x = list[i].replace(/^\s*!\s*/, '');
15                 if (x == list[i])
16                     continue;
17
18                 if (this.strGlob(x, group))
19                     return false;
20             }
21
22             for (var i = 0; i < list.length; i++)
23             {
24                 var x = list[i].replace(/^\s*!\s*/, '');
25                 if (x != list[i])
26                     continue;
27
28                 if (this.strGlob(x, group))
29                     return true;
30             }
31
32             return false;
33         },
34
35         aclTest: function(list, group) {
36             for (var i = 0; i < list.length; i++)
37                 if (list[i] == group)
38                     return true;
39
40             return false;
41         },
42
43         aclEqual: function(list1, list2) {
44             if (list1.length != list2.length)
45                 return false;
46
47             for (var i = 0; i < list1.length; i++)
48                 if (list1[i] != list2[i])
49                     return false;
50
51             return true;
52         },
53
54         aclFromUCI: function(value) {
55             var list;
56             if (typeof(value) == 'string')
57                 list = value.split(/\s+/);
58             else if ($.isArray(value))
59                 list = value;
60             else
61                 list = [ ];
62
63             var rv = [ ];
64             if (this.choices)
65                 for (var i = 0; i < this.choices.length; i++)
66                     if (this.aclMatch(list, this.choices[i][0]))
67                         rv.push(this.choices[i][0]);
68
69             return rv;
70         },
71
72         aclToUCI: function(list) {
73             if (list.length < (this.choices.length / 2))
74                 return list;
75
76             var set = { };
77             for (var i = 0; i < list.length; i++)
78                 set[list[i]] = true;
79
80             var rv = [ '*' ];
81             for (var i = 0; i < this.choices.length; i++)
82                 if (!set[this.choices[i][0]])
83                     rv.push('!' + this.choices[i][0]);
84
85             return rv;
86         },
87
88         setAll: function(ev) {
89             $(this).parents('table')
90                 .find('input[value=%d]'.format(ev.data.level))
91                 .prop('checked', true);
92         },
93
94                 widget: function(sid)
95                 {
96             var t = $('<table />')
97                 .addClass('table table-condensed table-hover')
98                 .attr('id', this.id(sid))
99                 .append($('<tr />')
100                     .append($('<th />')
101                         .text(L.tr('ACL Group')))
102                     .append($('<th />')
103                         .text(L.trc('No access', 'N'))
104                         .attr('title', L.tr('Set all to no access'))
105                         .css('cursor', 'pointer')
106                         .click({ level: 0 }, this.setAll))
107                     .append($('<th />')
108                         .text(L.trc('Read only access', 'R'))
109                         .attr('title', L.tr('Set all to read only access'))
110                         .css('cursor', 'pointer')
111                         .click({ level: 1 }, this.setAll))
112                     .append($('<th />')
113                         .text(L.trc('Full access', 'F'))
114                         .attr('title', L.tr('Set all to full access'))
115                         .css('cursor', 'pointer')
116                         .click({ level: 2 }, this.setAll)));
117
118             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
119             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
120
121             if (this.choices)
122                 for (var i = 0; i < this.choices.length; i++)
123                 {
124                     var r = t.get(0).insertRow(-1);
125                     var is_r = this.aclTest(acl_r, this.choices[i][0]);
126                     var is_w = this.aclTest(acl_w, this.choices[i][0]);
127
128                     $(r.insertCell(-1))
129                         .text(this.choices[i][1]);
130
131                     for (var j = 0; j < 3; j++)
132                     {
133                         $(r.insertCell(-1))
134                             .append($('<input />')
135                                 .addClass('form-control')
136                                 .attr('type', 'radio')
137                                 .attr('name', '%s_%s'.format(this.id(sid), this.choices[i][0]))
138                                 .attr('value', j)
139                                 .prop('checked', (j == 0 && !is_r && !is_w) ||
140                                                  (j == 1 &&  is_r && !is_w) ||
141                                                  (j == 2 &&           is_w)));
142                     }
143                 }
144
145             return t;
146                 },
147
148         textvalue: function(sid)
149         {
150             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
151             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
152
153             var htmlid = this.id(sid);
154             var radios = $('#' + htmlid + ' input');
155
156             var acls = [  ];
157
158             for (var i = 0; i < this.choices.length; i++)
159             {
160                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
161                 {
162                 case '2':
163                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('Full access', 'F')));
164                     break;
165
166                 case '1':
167                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('Read only access', 'R')));
168                     break;
169
170                 case '0':
171                     acls.push('%s: %s'.format(this.choices[i][0], L.trc('No access', 'N')));
172                     break;
173                 }
174             }
175
176             return acls.join(', ');
177         },
178
179                 value: function(k, v)
180                 {
181                         if (!this.choices)
182                                 this.choices = [ ];
183
184                         this.choices.push([k, v || k]);
185                         return this;
186                 },
187
188         save: function(sid)
189         {
190             var acl_r = this.aclFromUCI(this.map.get('rpcd', sid, 'read'));
191             var acl_w = this.aclFromUCI(this.map.get('rpcd', sid, 'write'));
192
193             var acl_r_new = [ ];
194             var acl_w_new = [ ];
195
196             var htmlid = this.id(sid);
197             var radios = $('#' + htmlid + ' input');
198
199             for (var i = 0; i < this.choices.length; i++)
200             {
201                 switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
202                 {
203                 case '2':
204                     acl_r_new.push(this.choices[i][0]);
205                     acl_w_new.push(this.choices[i][0]);
206                     break;
207
208                 case '1':
209                     acl_r_new.push(this.choices[i][0]);
210                     break;
211                 }
212             }
213
214             if (!this.aclEqual(acl_r, acl_r_new))
215                 this.map.set('rpcd', sid, 'read', this.aclToUCI(acl_r_new));
216
217             if (!this.aclEqual(acl_w, acl_w_new))
218                 this.map.set('rpcd', sid, 'write', this.aclToUCI(acl_w_new));
219         }
220         }),
221
222     execute: function() {
223         var self = this;
224         return L.ui.listAvailableACLs().then(function(acls) {
225             var m = new L.cbi.Map('rpcd', {
226                 caption:     L.tr('Guest Logins'),
227                 description: L.tr('Manage user accounts and permissions for accessing the LuCI ui.'),
228                 readonly:    !self.options.acls.users
229             });
230
231             var s = m.section(L.cbi.TypedSection, 'login', {
232                 caption:      L.tr('Accounts'),
233                 collabsible:  true,
234                 addremove:    true,
235                 add_caption:  L.tr('Add account …'),
236                 teasers:      [ 'username', '__shadow', '__acls' ]
237             });
238
239             s.option(L.cbi.InputValue, 'username', {
240                 caption:     L.tr('Username'),
241                 description: L.tr('Specifies the login name for the guest account'),
242                 optional:    false
243             });
244
245
246             var shadow = s.option(L.cbi.CheckboxValue, '__shadow', {
247                 caption:     L.tr('Use system account'),
248                 description: L.tr('Use password from the Linux user database')
249             });
250
251             shadow.ucivalue = function(sid) {
252                 var pw = this.map.get('rpcd', sid, 'password');
253                 return (pw && pw.indexOf('$p$') == 0);
254             };
255
256
257             var password = s.option(L.cbi.PasswordValue, 'password', {
258                 caption:     L.tr('Password'),
259                 description: L.tr('Specifies the password for the guest account. If you enter a plaintext password here, it will get replaced with a crypted password hash on save.'),
260                 optional:    false
261             });
262
263             password.depends('__shadow', false);
264
265             password.toggle = function(sid) {
266                 var id = '#' + this.id(sid);
267                 var pw = this.map.get('rpcd', sid, 'password');
268                 var sh = this.section.fields.__shadow.formvalue(sid);
269
270                 if (!sh && pw && pw.indexOf('$p$') == 0)
271                     $(id).val('');
272
273                 this.callSuper('toggle', sid);
274             };
275
276             shadow.save = password.save = function(sid) {
277                 var sh = this.section.fields.__shadow.formvalue(sid);
278                 var pw = this.section.fields.password.formvalue(sid);
279
280                 if (!sh && !pw)
281                     return;
282
283                 if (sh)
284                     pw = '$p$' + this.section.fields.username.formvalue(sid);
285
286                 if (pw.match(/^\$[0-9p][a-z]?\$/))
287                 {
288                     if (pw != this.map.get('rpcd', sid, 'password'))
289                         this.map.set('rpcd', sid, 'password', pw);
290                 }
291                 else
292                 {
293                     var map = this.map;
294                     return L.ui.cryptPassword(pw).then(function(crypt) {
295                         map.set('rpcd', sid, 'password', crypt);
296                     });
297                 }
298             };
299
300             var o = s.option(self.aclTable, '__acls', {
301                 caption:     L.tr('User ACLs'),
302                 description: L.tr('Specifies the access levels of this account. The "N" column means no access, "R" stands for read only access and "F" for full access.')
303             });
304
305             var groups = [ ];
306             for (var group_name in acls)
307                 groups.push(group_name);
308
309             groups.sort();
310
311             for (var i = 0; i < groups.length; i++)
312                 o.value(groups[i], acls[groups[i]].description);
313
314             return m.insertInto('#map');
315         });
316     }
317 });