      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