1 // The Antville Project
  2 // http://code.google.com/p/antville
  3 //
  4 // Copyright 2007-2011 by Tobi Schäfer.
  5 //
  6 // Copyright 2001–2007 Robert Gaggl, Hannes Wallnöfer, Tobi Schäfer,
  7 // Matthias & Michael Platzer, Christoph Lincke.
  8 //
  9 // Licensed under the Apache License, Version 2.0 (the ``License'');
 10 // you may not use this file except in compliance with the License.
 11 // You may obtain a copy of the License at
 12 //
 13 //    http://www.apache.org/licenses/LICENSE-2.0
 14 //
 15 // Unless required by applicable law or agreed to in writing, software
 16 // distributed under the License is distributed on an ``AS IS'' BASIS,
 17 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 18 // See the License for the specific language governing permissions and
 19 // limitations under the License.
 20 //
 21 // $Revision$
 22 // $Author$
 23 // $Date$
 24 // $URL$
 25 
 26 /**
 27  * @fileOverview Defines the User prototype.
 28  */
 29 
 30 markgettext("User");
 31 markgettext("user");
 32 
 33 this.handleMetadata("hash");
 34 this.handleMetadata("salt");
 35 this.handleMetadata("url");
 36 
 37 disableMacro(User, "hash");
 38 disableMacro(User, "salt");
 39 
 40 /** @constant */
 41 User.COOKIE = getProperty("userCookie", "antvilleUser");
 42 
 43 /** @constant */
 44 User.HASHCOOKIE = getProperty("hashCookie", "antvilleHash");
 45 
 46 /**
 47  * FIXME: Still needs a solution whether and how to remove a user’s sites
 48  */
 49 User.remove = function() {
 50    return; // FIXME: Disabled until thoroughly tested
 51    if (this.constructor === User) {
 52       HopObject.remove.call(this.comments);
 53       HopObject.remove.call(this.files);
 54       HopObject.remove.call(this.images);
 55       //HopObject.remove.call(this.sites);
 56       HopObject.remove.call(this.stories);
 57       this.deleteMetadata();
 58       this.remove();
 59    }
 60    return;
 61 }
 62 
 63 /**
 64  * 
 65  * @param {String} name
 66  * @returns {User}
 67  */
 68 User.getByName = function(name) {
 69    return root.users.get(name);
 70 }
 71 
 72 /**
 73  * @function
 74  * @returns {String[]}
 75  * @see defineConstants
 76  */
 77 User.getStatus = defineConstants(User, markgettext("Blocked"), 
 78       markgettext("Regular"), markgettext("Trusted"), 
 79       markgettext("Privileged"));
 80 
 81 /**
 82  * @returns {String}
 83  */
 84 User.getSalt = function() {
 85    var salt = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8);
 86    var random = java.security.SecureRandom.getInstance("SHA1PRNG");
 87    random.nextBytes(salt);
 88    return Packages.sun.misc.BASE64Encoder().encode(salt);
 89 }
 90 
 91 /**
 92  * 
 93  * @param {Object} data
 94  * @throws {Error}
 95  * @returns {User}
 96  */
 97 User.register = function(data) {
 98    if (!data.name) {
 99       throw Error(gettext("Please enter a username."));
100    }
101 
102    data.name = data.name.trim();
103    if (data.name.length > 30) {
104       throw Error(gettext("Sorry, the username you entered is too long. Please choose a shorter one."));
105    } else if (data.name !== stripTags(data.name) || NAMEPATTERN.test(data.name)) {
106       throw Error(gettext("Please avoid special characters or HTML code in the name field."));
107    } else if (data.name !== root.users.getAccessName(data.name)) {
108       throw Error(gettext("Sorry, the user name you entered already exists. Please enter a different one."));
109    }
110 
111    data.email && (data.email = data.email.trim());
112    if (!validateEmail(data.email)) {
113       throw Error(gettext("Please enter a valid e-mail address"));
114    }
115 
116    if (User.isBlacklisted(data)) {
117       throw Error("Sequere pecuniam ad meliora.");
118    }
119 
120    // Create hash from password for JavaScript-disabled browsers
121    if (!data.hash) {
122       // Check if passwords match
123       if (!data.password || !data.passwordConfirm) {
124          throw Error(gettext("Could not verify your password. Please repeat your input."))
125       } else if (data.password !== data.passwordConfirm) {
126          throw Error(gettext("Unfortunately, your passwords did not match. Please repeat your input."));
127       }
128       data.hash = (data.password + session.data.token).md5();
129    }
130 
131    var user = new User(data);
132    // grant trust and sysadmin-rights if there's no sysadmin 'til now
133    if (root.admins.size() < 1) {
134       user.status = User.PRIVILEGED;
135    }
136    root.users.add(user);
137    session.login(user);
138    return user;
139 }
140 
141 /**
142  * 
143  * @param {Object} data
144  * @returns {Boolean}
145  */
146 User.isBlacklisted = function(data) {
147    var url;
148    var name = encodeURIComponent(data.name);
149    var email = encodeURIComponent(data.email);
150    var ip = encodeURIComponent(data.http_remotehost);
151 
152    var key = getProperty("botscout.apikey");
153    if (key) {
154       url = ["http://botscout.com/test/?multi", "&key=", key, "&mail=", email, "&ip=", ip];
155       try {
156          mime = getURL(url.join(String.EMPTY));
157          if (mime && mime.text && mime.text.startsWith("Y")) {
158             return true;
159          }
160       } catch (ex) {
161          app.log("Exception while trying to check blacklist URL " + url);
162          app.log(ex);
163       }
164    }
165    //return false;
166 
167    // We only get here if botscout.com does not already blacklist the ip or email address
168    url = ["http://www.stopforumspam.com/api?f=json", "&email=", email];
169    if (ip.match(/^(?:\d{1,3}\.){3}\d{1,3}$/)) {
170       url.push("&ip=", ip);
171    }
172    try {
173       mime = getURL(url.join(String.EMPTY));
174    } catch (ex) {
175       app.log("Exception while trying to check blacklist URL " + url);
176       app.log(ex);
177    }
178    if (mime && mime.text) {
179       var result = JSON.parse(mime.text);
180       if (result.success) {
181          return !!(result.email.appears || (result.ip && result.ip.appears));
182       }
183    }
184    return false;
185 }
186 
187 /**
188  * 
189  */
190 User.autoLogin = function() {
191    if (session.user) {
192       return;
193    }
194    var name = req.cookies[User.COOKIE];
195    var hash = req.cookies[User.HASHCOOKIE];
196    if (!name || !hash) {
197       return;
198    }
199    var user = User.getByName(name);
200    if (!user) {
201       return;
202    }
203    var ip = req.data.http_remotehost.clip(getProperty("cookieLevel", "4"),
204          String.EMPTY, "\\.");
205    if ((user.hash + ip).md5() !== hash) {
206       return;
207    }
208    session.login(user);
209    user.touch();
210    res.message = gettext('Welcome to {0}, {1}. Have fun!',
211          res.handlers.site.title, user.name);
212    return;
213 }
214 
215 /**
216  * 
217  * @param {Object} data
218  * @returns {User}
219  */
220 User.login = function(data) {
221    var user = User.getByName(data.name);
222    if (!user) {
223       throw Error(gettext("Unfortunately, your login failed. Maybe a typo?"));
224    }
225    var digest = data.digest;
226    // Calculate digest for JavaScript-disabled browsers
227    if (!digest) {
228       app.logger.warn("Received clear text password from " + req.data.http_referer);
229       digest = ((data.password + user.salt).md5() + session.data.token).md5();
230    }
231    // Check if login is correct
232    if (digest !== user.getDigest(session.data.token)) {
233       throw Error(gettext("Unfortunately, your login failed. Maybe a typo?"))
234    }
235    if (data.remember) {
236       // Set long running cookies for automatic login
237       res.setCookie(User.COOKIE, user.name, 365);
238       var ip = req.data.http_remotehost.clip(getProperty("cookieLevel", "4"), String.EMPTY, "\\.");
239       res.setCookie(User.HASHCOOKIE, (user.hash + ip).md5(), 365);
240    }
241    user.touch();
242    session.login(user);
243    return user;
244 }
245 
246 /**
247  * 
248  */
249 User.logout = function() {
250   session.logout();
251   res.setCookie(User.COOKIE, String.EMPTY);
252   res.setCookie(User.HASHCOOKIE, String.EMPTY);
253   User.getLocation();
254   return;
255 }
256 
257 /**
258  * 
259  * @param {String} requiredStatus
260  * @returns {Boolean}
261  */
262 User.require = function(requiredStatus) {
263    var status = [User.BLOCKED, User.REGULAR, User.TRUSTED, User.PRIVILEGED];
264    if (requiredStatus && session.user) {
265       return status.indexOf(session.user.status) >= status.indexOf(requiredStatus);
266    }
267    return false;
268 }
269 
270 /**
271  * @returns {String}
272  */
273 User.getCurrentStatus = function() {
274    if (session.user) {
275       return session.user.status;
276    }
277    return null;
278 }
279 
280 /**
281  * @returns {Membership}
282  */
283 User.getMembership = function() {
284    var membership;
285    if (session.user) {
286       membership = Membership.getByName(session.user.name);
287    }
288    return membership || new Membership;
289 }
290 
291 /**
292  * 
293  * @param {String} url
294  */
295 User.setLocation = function(url) {
296    session.data.location = url || req.data.http_referer;
297    //app.debug("Pushed location " + session.data.location);
298    return;
299 }
300 
301 /**
302  * @returns {String}
303  */
304 User.getLocation = function() {
305    var url = session.data.location;
306    delete session.data.location;
307    //app.debug("Popped location " + url);
308    return url;
309 }
310 
311 /**
312  * Rename a user account.
313  * @param {String} currentName The current name of the user account.
314  * @param {String} newName The desired name of the user account.
315  */
316 User.rename = function(currentName, newName) {
317    var user = User.getByName(currentName);
318    if (user) {
319       if (user.name === newName) {
320          return newName;
321       }
322       user.name = root.users.getAccessName(newName);
323       return user.name;
324    }
325    return currentName;
326 }
327 
328 /**
329  * A User object represents a login to Antville.
330  * @name User
331  * @constructor
332  * @param {Object} data
333  * @extends HopObject
334  * @property {Membership[]} _children
335  * @property {Date} created
336  * @property {Comment[]} comments
337  * @property {String} email
338  * @property {File[]} files
339  * @property {String} hash
340  * @property {Image[]} images
341  * @property {Membership[]} memberships
342  * @property {Metadata} metadata
343  * @property {Date} modified
344  * @property {String} name 
345  * @property {String} salt
346  * @property {Site[]} sites
347  * @property {Membership[]} subscriptions
348  * @property {String} status
349  * @property {Story[]} stories
350  * @property {String} url 
351  * @extends HopObject
352  */
353 User.prototype.constructor = function(data) {
354    var now = new Date;
355    // FIXME: Refactor to be able to call constructor w/o arguments; use User.update() instead
356    this.map({
357       created: now,
358       email: data.email,
359       hash: data.hash,
360       modified: now,
361       name: data.name,
362       salt: session.data.token,
363       status: User.REGULAR,
364       url: data.url
365    });
366    return this;
367 }
368 
369 /**
370  * 
371  */
372 User.prototype.onLogout = function() { /* ... */ }
373 
374 /**
375  * 
376  * @param {String} action
377  * @returns {Boolean}
378  */
379 User.prototype.getPermission = function(action) {
380    return User.require(User.PRIVILEGED);
381 }
382 
383 /**
384  * 
385  * @param {Object} data
386  */
387 User.prototype.update = function(data) {
388    if (!data.digest && data.password) {
389       data.digest = ((data.password + this.salt).md5() + 
390             session.data.token).md5();
391    }
392    if (data.digest) {
393       if (data.digest !== this.getDigest(session.data.token)) {
394          throw Error(gettext("Oops, your old password is incorrect. Please re-enter it."));
395       }
396       if (!data.hash) {
397          if (!data.newPassword || !data.newPasswordConfirm) {
398             throw Error(gettext("Please specify a new password."));
399          } else if (data.newPassword !== data.newPasswordConfirm) {
400             throw Error(gettext("Unfortunately, your passwords did not match. Please repeat your input."));
401          }
402          data.hash = (data.newPassword + session.data.token).md5();
403       }
404       this.map({
405          hash: data.hash,
406          salt: session.data.token         
407       });
408    }
409    if (!(data.email = validateEmail(data.email))) {
410       throw Error(gettext("Please enter a valid e-mail address"));
411    }
412    if (data.url && !(data.url = validateUrl(data.url))) {
413       throw Error(gettext("Please enter a valid URL"));
414    }
415    this.email = data.email;
416    this.url = data.url;
417    this.touch();
418    return this;
419 }
420 
421 /**
422  * 
423  */
424 User.prototype.touch = function() {
425    this.modified = new Date;
426    return;
427 }
428 
429 /**
430  * 
431  * @param {String} token
432  * @returns {String}
433  */
434 User.prototype.getDigest = function(token) {
435    token || (token = String.EMPTY);
436    return (this.hash + token).md5();
437 }
438 
439 /**
440  * 
441  * @param {String} name
442  * @returns {Object}
443  */
444 User.prototype.getFormOptions = function(name) {
445    switch (name) {
446       case "status":
447       return User.getStatus();
448    }
449 }
450 
451 /**
452  * Enable <% user.email %> macro for privileged users only
453  */
454 User.prototype.email_macro = function() {
455    if (User.require(User.PRIVILEGED)) {
456       res.write(this.email);
457    }
458    return;
459 }
460 
461 /**
462  * 
463  * @param {Object} param
464  * @param {String} type
465  */
466 User.prototype.list_macro = function(param, type) {
467    switch (type) {
468       case "sites":
469       var memberships = session.user.list();
470       memberships.sort(function(a, b) {
471          return b.site.modified - a.site.modified;
472       });
473       memberships.forEach(function(membership) {
474          var site;
475          if (site = membership.get("site")) {
476             site.renderSkin("$Site#listItem");
477          }
478          return;
479       });
480    }
481    return;
482 }
483