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