      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