1 // 2 // The Antville Project 3 // http://code.google.com/p/antville 4 // 5 // Copyright 2001-2007 by The Antville People 6 // 7 // Licensed under the Apache License, Version 2.0 (the ``License''); 8 // you may not use this file except in compliance with the License. 9 // You may obtain a copy of the License at 10 // 11 // http://www.apache.org/licenses/LICENSE-2.0 12 // 13 // Unless required by applicable law or agreed to in writing, software 14 // distributed under the License is distributed on an ``AS IS'' BASIS, 15 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 // See the License for the specific language governing permissions and 17 // limitations under the License. 18 // 19 // $Revision$ 20 // $LastChangedBy$ 21 // $LastChangedDate$ 22 // $URL$ 23 // 24 25 /** 26 * @fileOverview Defines the User prototype. 27 */ 28 29 /** @constant */ 30 User.COOKIE = getProperty("userCookie", "antvilleUser"); 31 /** @constant */ 32 User.HASHCOOKIE = getProperty("hashCookie", "antvilleHash"); 33 34 /** 35 * @function 36 * @returns {String[]} 37 * @see defineConstants 38 */ 39 User.getStatus = defineConstants(User, "blocked", 40 "regular", "trusted", "privileged"); 41 42 this.handleMetadata("hash"); 43 this.handleMetadata("salt"); 44 this.handleMetadata("url"); 45 46 disableMacro(User, "password"); 47 disableMacro(User, "hash"); 48 disableMacro(User, "email"); 49 50 /** 51 * A User object represents a login to Antville. 52 * @name User 53 * @constructor 54 * @param {Object} data 55 * @extends HopObject 56 * @property {Membership[]} _children 57 * @property {Date} created 58 * @property {Comment[]} comments 59 * @property {String} email 60 * @property {File[]} files 61 * @property {String} hash 62 * @property {Image[]} images 63 * @property {Membership[]} memberships 64 * @property {Metadata} metadata 65 * @property {Date} modified 66 * @property {String} name 67 * @property {String} salt 68 * @property {Site[]} sites 69 * @property {Membership[]} subscriptions 70 * @property {String} status 71 * @property {Story[]} stories 72 * @extends HopObject 73 */ 74 User.prototype.constructor = function(data) { 75 var now = new Date; 76 this.map({ 77 name: data.name, 78 hash: data.hash, 79 salt: session.data.token, 80 email: data.email, 81 status: User.REGULAR, 82 created: now, 83 modified: now 84 }); 85 return this; 86 } 87 88 /** 89 * 90 */ 91 User.prototype.onLogout = function() { /* ... */ } 92 93 /** 94 * 95 * @param {String} action 96 * @returns {Boolean} 97 */ 98 User.prototype.getPermission = function(action) { 99 return User.require(User.PRIVILEGED); 100 } 101 102 /** 103 * 104 * @param {Object} data 105 */ 106 User.prototype.update = function(data) { 107 if (!data.digest && data.password) { 108 data.digest = ((data.password + this.salt).md5() + 109 session.data.token).md5(); 110 } 111 if (data.digest) { 112 if (data.digest !== this.getDigest(session.data.token)) { 113 throw Error(gettext("Oops, your old password is incorrect. Please re-enter it.")); 114 } 115 if (!data.hash) { 116 if (!data.newPassword || !data.newPasswordConfirm) { 117 throw Error(gettext("Please specify a new password.")); 118 } else if (data.newPassword !== data.newPasswordConfirm) { 119 throw Error(gettext("Unfortunately, your passwords did not match. Please repeat your input.")); 120 } 121 data.hash = (data.newPassword + session.data.token).md5(); 122 } 123 this.map({ 124 hash: data.hash, 125 salt: session.data.token 126 }); 127 } 128 if (!(this.email = validateEmail(data.email))) { 129 throw Error(gettext("Please enter a valid e-mail address")); 130 } 131 132 if(data.url != "") { 133 if (!(this.url = validateUrl(data.url))) { 134 throw Error(gettext("Please enter a valid URL")); 135 } 136 } 137 return this; 138 } 139 140 /** 141 * 142 */ 143 User.prototype.touch = function() { 144 this.modified = new Date; 145 return; 146 } 147 148 /** 149 * 150 * @param {String} token 151 * @returns {String} 152 */ 153 User.prototype.getDigest = function(token) { 154 token || (token = String.EMPTY); 155 return (this.hash + token).md5(); 156 } 157 158 /** 159 * 160 * @param {String} name 161 * @returns {Object} 162 */ 163 User.prototype.getFormOptions = function(name) { 164 switch (name) { 165 case "status": 166 return User.getStatus(); 167 } 168 } 169 170 /** 171 * 172 * @param {Object} param 173 * @param {String} type 174 */ 175 User.prototype.list_macro = function(param, type) { 176 switch (type) { 177 case "sites": 178 var memberships = session.user.list(); 179 memberships.sort(function(a, b) { 180 return b.site.modified - a.site.modified; 181 }); 182 memberships.forEach(function(membership) { 183 var site; 184 if (site = membership.get("site")) { 185 site.renderSkin("$Site#listItem"); 186 } 187 return; 188 }); 189 } 190 return; 191 } 192 193 /** 194 * 195 * @param {String} name 196 * @returns {User} 197 */ 198 User.getByName = function(name) { 199 return root.users.get(name); 200 } 201 202 /** 203 * @returns {String} 204 */ 205 User.getSalt = function() { 206 var salt = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 8);; 207 var random = java.security.SecureRandom.getInstance("SHA1PRNG"); 208 random.nextBytes(salt); 209 return Packages.sun.misc.BASE64Encoder().encode(salt); 210 } 211 212 /** 213 * 214 * @param {Object} data 215 * @throws {Error} 216 * @returns {User} 217 */ 218 User.register = function(data) { 219 // check if username is existing and is clean 220 // can't use isClean() here because we accept 221 // special characters like . - and space 222 var invalidChar = new RegExp("[^a-zA-Z0-9\\.\\-_ ]"); 223 if (!data.name) { 224 throw Error(gettext("Please enter a username.")); 225 } else if (data.name.length > 30) { 226 throw Error(gettext("Sorry, the username you entered is too long. Please choose a shorter one.")); 227 } else if (invalidChar.exec(data.name)) { 228 throw Error(gettext("Please enter alphanumeric characters only in the username field.")); 229 } else if (this[data.name] || this[data.name + "_action"]) { 230 throw Error(gettext("Sorry, the user name you entered already exists. Please enter a different one.")); 231 } 232 233 // check if email-address is valid 234 if (!data.email) { 235 throw Error(gettext("Please enter your e-mail address.")); 236 } 237 validateEmail(data.email); 238 239 // Create hash from password for JavaScript-disabled browsers 240 if (!data.hash) { 241 // Check if passwords match 242 if (!data.password || !data.passwordConfirm) { 243 throw Error(gettext("Could not verify your password. Please repeat your input.")) 244 } else if (data.password !== data.passwordConfirm) { 245 throw Error(gettext("Unfortunately, your passwords did not match. Please repeat your input.")); 246 } 247 data.hash = (data.password + session.data.token).md5(); 248 } 249 250 if (User.getByName(data.name)) { 251 throw Error(gettext("Sorry, the user name you entered already exists. Please enter a different one.")); 252 } 253 var user = new User(data); 254 // grant trust and sysadmin-rights if there's no sysadmin 'til now 255 if (root.admins.size() < 1) { 256 user.status = User.PRIVILEGED; 257 } 258 root.users.add(user); 259 session.login(user); 260 return user; 261 } 262 263 /** 264 * 265 */ 266 User.autoLogin = function() { 267 if (session.user) { 268 return; 269 } 270 var name = req.cookies[User.COOKIE]; 271 var hash = req.cookies[User.HASHCOOKIE]; 272 if (!name || !hash) { 273 return; 274 } 275 var user = User.getByName(name); 276 if (!user) { 277 return; 278 } 279 var ip = req.data.http_remotehost.clip(getProperty("cookieLevel", "4"), 280 "", "\\."); 281 if ((user.hash + ip).md5() !== hash) { 282 return; 283 } 284 session.login(user); 285 user.touch(); 286 res.message = gettext('Welcome to {0}, {1}. Have fun!', 287 res.handlers.site.title, user.name); 288 return; 289 } 290 291 /** 292 * 293 * @param {Object} data 294 * @returns {User} 295 */ 296 User.login = function(data) { 297 var user = User.getByName(data.name); 298 if (!user) { 299 throw Error(gettext("Unfortunately, your login failed. Maybe a typo?")); 300 } 301 var digest = data.digest; 302 // Calculate digest for JavaScript-disabled browsers 303 if (!digest) { 304 app.logger.warn("Received clear text password from " + req.data.http_referer); 305 digest = ((data.password + user.salt).md5() + session.data.token).md5(); 306 } 307 // Check if login is correct 308 if (digest !== user.getDigest(session.data.token)) { 309 throw Error(gettext("Unfortunately, your login failed. Maybe a typo?")) 310 } 311 if (data.remember) { 312 // Set long running cookies for automatic login 313 res.setCookie(User.COOKIE, user.name, 365); 314 var ip = req.data.http_remotehost.clip(getProperty("cookieLevel", "4"), 315 "", "\\."); 316 res.setCookie(User.HASHCOOKIE, (user.hash + ip).md5(), 365); 317 } 318 user.touch(); 319 session.login(user); 320 return user; 321 } 322 323 /** 324 * 325 */ 326 User.logout = function() { 327 session.logout(); 328 res.setCookie(User.COOKIE, String.EMPTY); 329 res.setCookie(User.HASHCOOKIE, String.EMPTY); 330 User.getLocation(); 331 return; 332 } 333 334 /** 335 * 336 * @param {String} requiredStatus 337 * @returns {Boolean} 338 */ 339 User.require = function(requiredStatus) { 340 var status = [User.BLOCKED, User.REGULAR, User.TRUSTED, User.PRIVILEGED]; 341 if (requiredStatus && session.user) { 342 return status.indexOf(session.user.status) >= status.indexOf(requiredStatus); 343 } 344 return false; 345 } 346 347 /** 348 * @returns {String} 349 */ 350 User.getCurrentStatus = function() { 351 if (session.user) { 352 return session.user.status; 353 } 354 return null; 355 } 356 357 /** 358 * @returns {Membership} 359 */ 360 User.getMembership = function() { 361 var membership; 362 if (session.user) { 363 membership = Membership.getByName(session.user.name); 364 } 365 return membership || new Membership; 366 } 367 368 /** 369 * 370 * @param {String} url 371 */ 372 User.setLocation = function(url) { 373 session.data.location = url || req.data.http_referer; 374 //app.debug("Pushed location " + session.data.location); 375 return; 376 } 377 378 /** 379 * @returns {String} 380 */ 381 User.getLocation = function() { 382 var url = session.data.location; 383 delete session.data.location; 384 //app.debug("Popped location " + url); 385 return url; 386 } 387