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 extensions of Helma’s built-in 27 * HopObject prototype. 28 */ 29 30 /** 31 * 32 * @param {HopObject} collection 33 * @param {Object} options Optional flags, e.g. to force or prevent any 34 * conditional checks of individual prototype’s remove() methods 35 */ 36 HopObject.remove = function(options) { 37 var item; 38 while (this.size() > 0) { 39 item = this.get(0); 40 if (item.constructor.remove) { 41 item.constructor.remove.call(item, options); 42 } else if (!options) { 43 item.remove(); 44 } else { 45 throw Error("Missing static " + item.constructor.name + 46 ".remove() method"); 47 } 48 } 49 return; 50 } 51 52 /** 53 * 54 * @param {String} name 55 * @param {HopObject} collection 56 */ 57 HopObject.getFromPath = function(name, collection) { 58 if (name) { 59 var site; 60 if (name.contains("/")) { 61 var parts = name.split("/"); 62 site = root.get(parts[0]); 63 name = parts[1]; 64 } else { 65 site = res.handlers.site; 66 } 67 if (site && site.getPermission("main")) { 68 return site[collection].get(name); 69 } 70 } 71 return null; 72 } 73 74 /** 75 * Helma’s built-in HopObject with Antville’s extensions. 76 * @name HopObject 77 * @constructor 78 */ 79 80 /** 81 * 82 * @param {Object} values 83 */ 84 HopObject.prototype.map = function(values) { 85 for (var i in values) { 86 this[i] = values[i]; 87 } 88 return; 89 } 90 91 /** 92 * 93 */ 94 HopObject.prototype.onRequest = function() { 95 // Checking if we are on the correct host to prevent at least some XSS issues 96 if (req.action !== "notfound" && req.action !== "error" && 97 this.href().startsWith("http://") && 98 !this.href().toLowerCase().startsWith(req.servletRequest.scheme + 99 "://" + req.servletRequest.serverName.toLowerCase())) { 100 res.redirect(this.href(req.action === "main" ? String.EMPTY : req.action)); 101 } 102 103 User.autoLogin(); 104 res.handlers.membership = User.getMembership(); 105 106 if (User.getCurrentStatus() === User.BLOCKED) { 107 session.data.status = 403; 108 session.data.error = gettext("Your account has been blocked.") + String.SPACE + 109 gettext("Please contact an administrator for further information."); 110 User.logout(); 111 res.redirect(root.href("error")); 112 } 113 114 if (res.handlers.site.status === Site.BLOCKED && 115 !User.require(User.PRIVILEGED)) { 116 session.data.status = 403; 117 session.data.error = gettext("The site you requested has been blocked.") + 118 String.SPACE + gettext("Please contact an administrator for further information."); 119 res.redirect(root.href("error")); 120 } 121 122 res.handlers.layout = res.handlers.site.layout || new Layout; 123 res.skinpath = res.handlers.layout.getSkinPath(); 124 125 if (!this.getPermission(req.action)) { 126 if (!session.user) { 127 User.setLocation(root.href() + req.path); 128 res.message = gettext("Please login first."); 129 res.redirect(res.handlers.site.members.href("login")); 130 } 131 User.getLocation(); 132 res.status = 401; 133 res.data.title = gettext("{0} 401 Error", root.title); 134 res.data.body = root.renderSkinAsString("$Root#error", {error: 135 gettext("You are not allowed to access this part of the site.")}); 136 res.handlers.site.renderSkin("Site#page"); 137 session.data.error = null; 138 res.stop(); 139 } 140 141 res.meta.values = {}; 142 res.handlers.site.renderSkinAsString("Site#values"); 143 return; 144 } 145 146 /** 147 * @returns Boolean 148 */ 149 HopObject.prototype.getPermission = function() { 150 return true; 151 } 152 153 // Marking some prototype names used in res.message of HopObject.delete_action() 154 markgettext("Comment"); 155 markgettext("File"); 156 markgettext("Image"); 157 markgettext("Membership"); 158 markgettext("Poll"); 159 markgettext("Story"); 160 161 HopObject.prototype.delete_action = function() { 162 if (req.postParams.proceed) { 163 try { 164 var parent = this._parent; 165 var url = this.constructor.remove.call(this, req.postParams) || 166 parent.href(); 167 res.message = gettext("{0} was successfully deleted.", gettext(this._prototype)); 168 res.redirect(User.getLocation() || url); 169 } catch(ex) { 170 res.message = ex; 171 app.log(ex); 172 } 173 } 174 175 res.data.action = this.href(req.action); 176 res.data.title = gettext("Confirm Deletion"); 177 res.data.body = this.renderSkinAsString("$HopObject#confirm", { 178 text: this.getConfirmText() 179 }); 180 res.handlers.site.renderSkin("Site#page"); 181 return; 182 } 183 184 /** 185 * @returns {Object} 186 */ 187 HopObject.prototype.touch = function() { 188 return this.map({ 189 modified: new Date, 190 modifier: session.user 191 }); 192 } 193 194 /** 195 * 196 */ 197 HopObject.prototype.log = function() { 198 var entry = new LogEntry(this, "main"); 199 app.data.entries.push(entry); 200 return; 201 } 202 203 /** 204 * 205 * @param {String} action 206 */ 207 HopObject.prototype.notify = function(action) { 208 var self = this; 209 var site = res.handlers.site; 210 211 var getPermission = function(scope, mode, status) { 212 if (scope === Admin.NONE || mode === Site.NOBODY || 213 status === Site.BLOCKED) { 214 return false; 215 } 216 var scopes = [Admin.REGULAR, Admin.TRUSTED]; 217 if (scopes.indexOf(status) < scopes.indexOf(scope)) { 218 return false; 219 } 220 if (!Membership.require(mode)) { 221 return false; 222 } 223 return true; 224 } 225 226 // Helper method for debugging 227 var renderMatrix = function() { 228 var buf = ['<table border=1 cellspacing=0>']; 229 for each (var scope in Admin.getNotificationScopes()) { 230 for each (var mode in Site.getNotificationModes()) { 231 for each (var status in Site.getStatus()) { 232 var perm = getPermission(scope.value, mode.value, status.value); 233 buf.push('<tr style="'); 234 perm && buf.push('color: blue;'); 235 if (scope.value === root.notificationScope && mode.value === 236 site.notificationMode && status.value === site.status) { 237 buf.push(' background-color: yellow;'); 238 } 239 buf.push('">'); 240 buf.push('<td>', scope.value, '</td>'); 241 buf.push('<td>', status.value, '</td>'); 242 buf.push('<td>', mode.value, '</td>'); 243 buf.push('<td>', perm, '</td>'); 244 buf.push('</tr>'); 245 } 246 } 247 } 248 buf.push('</table>'); 249 res.write(buf.join("")); 250 return; 251 } 252 253 switch (action) { 254 case "comment": 255 action = "create"; break; 256 } 257 258 var currentMembership = res.handlers.membership; 259 site.members.forEach(function() { 260 var membership = res.handlers.membership = this; 261 if (getPermission(root.notificationScope, site.notificationMode, site.status)) { 262 sendMail(membership.creator.email, gettext("[{0}] Notification of site changes", 263 root.title), self.renderSkinAsString("$HopObject#notify_" + action)); 264 } 265 }); 266 res.handlers.membership = currentMembership; 267 return; 268 } 269 270 /** 271 * @returns {Tag[]} 272 */ 273 HopObject.prototype.getTags = function() { 274 var tags = []; 275 if (typeof this.tags === "object") { 276 this.tags.list().forEach(function(item) { 277 item.tag && tags.push(item.tag.name); 278 }); 279 } 280 return tags; 281 } 282 283 /** 284 * 285 * @param {Tag[]|String} tags 286 */ 287 HopObject.prototype.setTags = function(tags) { 288 if (typeof this.tags !== "object") { 289 return String.EMPTY; 290 } 291 292 if (!tags) { 293 tags = []; 294 } else if (tags.constructor === String) { 295 tags = tags.split(/\s*,\s*/); 296 } 297 298 var diff = {}; 299 var tag; 300 for (var i in tags) { 301 // Trim and remove troublesome characters (like ../.. etc.) 302 // We call getAccessName with a virgin HopObject to allow most names 303 tag = tags[i] = this.getAccessName.call(new HopObject, File.getName(tags[i])); 304 if (tag && diff[tag] == null) { 305 diff[tag] = 1; 306 } 307 } 308 this.tags.forEach(function() { 309 if (!this.tag) { 310 return; 311 } 312 diff[this.tag.name] = (tags.indexOf(this.tag.name) < 0) ? this : 0; 313 }); 314 315 for (var tag in diff) { 316 switch (diff[tag]) { 317 case 0: 318 // Do nothing (tag already exists) 319 break; 320 case 1: 321 // Add tag to story 322 this.addTag(tag); 323 break; 324 default: 325 // Remove tag 326 this.removeTag(diff[tag]); 327 } 328 } 329 return; 330 } 331 332 /** 333 * 334 * @param {String} name 335 */ 336 HopObject.prototype.addTag = function(name) { 337 this.tags.add(new TagHub(name, this, session.user)); 338 return; 339 } 340 341 /** 342 * 343 * @param {String} tag 344 */ 345 HopObject.prototype.removeTag = function(tag) { 346 var parent = tag._parent; 347 if (parent.size() === 1) { 348 parent.remove(); 349 } 350 tag.remove(); 351 return; 352 } 353 354 /** 355 * 356 * @param {Object} param 357 * @param {String} name 358 */ 359 HopObject.prototype.skin_macro = function(param, name) { 360 if (!name) { 361 return; 362 } 363 if (name.contains("#")) { 364 this.renderSkin(name); 365 } else { 366 var prototype = this._prototype || "Global"; 367 this.renderSkin(prototype + "#" + name); 368 } 369 return; 370 } 371 372 /** 373 * 374 * @param {Object} param 375 * @param {String} name 376 */ 377 HopObject.prototype.input_macro = function(param, name) { 378 param.name = name; 379 param.id = name; 380 param.value = this.getFormValue(name); 381 return html.input(param); 382 } 383 384 /** 385 * 386 * @param {Object} param 387 * @param {String} name 388 */ 389 HopObject.prototype.textarea_macro = function(param, name) { 390 param.name = name; 391 param.id = name; 392 param.value = this.getFormValue(name); 393 return html.textArea(param); 394 } 395 396 /** 397 * 398 * @param {Object} param 399 * @param {String} name 400 */ 401 HopObject.prototype.select_macro = function(param, name) { 402 param.name = name; 403 param.id = name; 404 var options = this.getFormOptions(name); 405 if (options.length < 2) { 406 param.disabled = "disabled"; 407 } 408 return html.dropDown(param, options, this.getFormValue(name)); 409 } 410 411 /** 412 * 413 * @param {Object} param 414 * @param {String} name 415 */ 416 HopObject.prototype.checkbox_macro = function(param, name) { 417 param.name = name; 418 param.id = name; 419 var options = this.getFormOptions(name); 420 if (options.length < 2) { 421 param.disabled = "disabled"; 422 } 423 param.value = String((options[1] || options[0]).value); 424 param.selectedValue = String(this.getFormValue(name)); 425 var label = param.label; 426 delete param.label; 427 html.checkBox(param); 428 if (label) { 429 html.element("label", label, {"for": name}); 430 } 431 return; 432 } 433 434 /** 435 * 436 * @param {Object} param 437 * @param {String} name 438 */ 439 HopObject.prototype.radiobutton_macro = function(param, name) { 440 param.name = name; 441 param.id = name; 442 var options = this.getFormOptions(name); 443 if (options.length < 2) { 444 param.disabled = "disabled"; 445 } 446 param.value = String(options[0].value); 447 param.selectedValue = String(this.getFormValue(name)); 448 var label = param.label; 449 delete param.label; 450 html.radioButton(param); 451 if (label) { 452 html.element("label", label, {"for": name}); 453 } 454 return; 455 } 456 457 /** 458 * 459 * @param {Object} param 460 * @param {String} name 461 */ 462 HopObject.prototype.upload_macro = function(param, name) { 463 param.name = name; 464 param.id = name; 465 param.value = this.getFormValue(name); 466 renderSkin("$Global#upload", param); 467 return; 468 } 469 470 /** 471 * 472 * @param {Object} param 473 * @param {HopObject} [handler] 474 */ 475 HopObject.prototype.macro_macro = function(param, handler) { 476 var ctor = this.constructor; 477 if ([Story, Image, File, Poll].indexOf(ctor) > -1) { 478 res.encode("<% "); 479 res.write(handler || ctor.name.toLowerCase()); 480 res.write(String.SPACE); 481 res.write(quote(this.name || this._id)); 482 res.encode(" %>"); 483 } 484 return; 485 } 486 487 /** 488 * 489 */ 490 HopObject.prototype.kind_macro = function() { 491 var type = this.constructor.name.toLowerCase(); 492 switch (type) { 493 default: 494 res.write(gettext(type)); 495 break; 496 } 497 return; 498 } 499 500 /** 501 * 502 * @param {String} name 503 * @returns {Number|String} 504 */ 505 HopObject.prototype.getFormValue = function(name) { 506 if (req.isPost()) { 507 return req.postParams[name]; 508 } else { 509 var value = this[name] || req.queryParams[name] || String.EMPTY; 510 return value instanceof HopObject ? value._id : value; 511 } 512 } 513 514 /** 515 * @returns {Object[]} 516 */ 517 HopObject.prototype.getFormOptions = function() { 518 return [{value: true, display: "enabled"}]; 519 } 520 521 /** 522 * @returns {HopObject} 523 * @param {Object} param 524 * @param {String} property 525 */ 526 HopObject.prototype.self_macro = function(param, property) { 527 return property ? this[property] : this; 528 } 529 530 /** 531 * 532 */ 533 HopObject.prototype.type_macro = function() { 534 return res.write(this.constructor.name); 535 } 536 537 /** 538 * 539 * @param {Object} param 540 * @param {String} url 541 * @param {String} text 542 */ 543 HopObject.prototype.link_macro = function(param, url, text) { 544 if (url && text) { 545 var action = url.split(/#|\?/)[0]; 546 if (this.getPermission(action)) { 547 renderLink.call(global, param, url, text, this); 548 } 549 } else { 550 res.write("[Insufficient link parameters]"); 551 } 552 return; 553 } 554 555 /** 556 * 557 * @param {Object} param 558 * @param {String} format 559 */ 560 HopObject.prototype.created_macro = function(param, format) { 561 if (this.isPersistent()) { 562 format || (format = param.format); 563 res.write(formatDate(this.created, format)); 564 } 565 return; 566 } 567 568 /** 569 * 570 * @param {Object} param 571 * @param {String} format 572 */ 573 HopObject.prototype.modified_macro = function(param, format) { 574 if (this.isPersistent()) { 575 format || (format = param.format); 576 res.write(formatDate(this.modified, format)); 577 } 578 return; 579 } 580 581 /** 582 * 583 * @param {Object} param 584 * @param {String} mode 585 */ 586 HopObject.prototype.creator_macro = function(param, mode) { 587 if (!this.creator || this.isTransient()) { 588 return; 589 } 590 mode || (mode = param.as); 591 if (mode === "link" && this.creator.url) { 592 html.link({href: this.creator.url}, this.creator.name); 593 } else if (mode === "url") { 594 res.write(this.creator.url); 595 } else { 596 res.write(this.creator.name); 597 } return; 598 } 599 600 /** 601 * 602 * @param {Object} param 603 * @param {String} mode 604 */ 605 HopObject.prototype.modifier_macro = function(param, mode) { 606 if (!this.modifier || this.isTransient()) { 607 return; 608 } 609 mode || (mode = param.as); 610 if (mode === "link" && this.modifier.url) { 611 html.link({href: this.modifier.url}, this.modifier.name); 612 } else if (mode === "url") { 613 res.write(this.modifier.url); 614 } else { 615 res.write(this.modifier.name); 616 } 617 return; 618 } 619 620 /** 621 * @returns {String} 622 */ 623 HopObject.prototype.getTitle = function() { 624 return this.title || gettext(this.__name__.capitalize()); 625 } 626 627 /** 628 * @returns {String} 629 */ 630 HopObject.prototype.toString = function() { 631 return this.constructor.name + " #" + this._id; 632 } 633 634 /** 635 * 636 * @param {String} text 637 * @param {Object} param 638 * @param {String} action 639 * @returns {String} 640 */ 641 HopObject.prototype.link_filter = function(text, param, action) { 642 action || (action = "."); 643 res.push(); 644 renderLink(param, action, text, this); 645 return res.pop(); 646 } 647 648 /** 649 * 650 * @param {String} name 651 */ 652 HopObject.prototype.handleMetadata = function(name) { 653 this.__defineGetter__(name, function() { 654 return this.metadata.get(name); 655 }); 656 this.__defineSetter__(name, function(value) { 657 return this.metadata.set(name, value); 658 }); 659 this[name + "_macro"] = function(param) { 660 var value; 661 if (value = this[name]) { 662 res.write(value); 663 } 664 return; 665 }; 666 return; 667 } 668