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