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