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