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 HopObject.prototype.delete_action = function() { 154 if (req.postParams.proceed) { 155 try { 156 var str = this.toString(); 157 var parent = this._parent; 158 var url = this.constructor.remove.call(this, req.postParams) || 159 parent.href(); 160 res.message = gettext("{0} was successfully deleted.", str); 161 res.redirect(User.getLocation() || url); 162 } catch(ex) { 163 res.message = ex; 164 app.log(ex); 165 } 166 } 167 168 res.data.action = this.href(req.action); 169 res.data.title = gettext("Confirm Deletion"); 170 res.data.body = this.renderSkinAsString("$HopObject#confirm", { 171 text: gettext('You are about to delete {0}.', this) 172 }); 173 res.handlers.site.renderSkin("Site#page"); 174 return; 175 } 176 177 /** 178 * @returns {Object} 179 */ 180 HopObject.prototype.touch = function() { 181 return this.map({ 182 modified: new Date, 183 modifier: session.user 184 }); 185 } 186 187 /** 188 * 189 */ 190 HopObject.prototype.log = function() { 191 var entry = new LogEntry(this, "main"); 192 app.data.entries.push(entry); 193 return; 194 } 195 196 /** 197 * 198 * @param {String} action 199 */ 200 HopObject.prototype.notify = function(action) { 201 var self = this; 202 var site = res.handlers.site; 203 204 var getPermission = function(scope, mode, status) { 205 if (scope === Admin.NONE || mode === Site.NOBODY || 206 status === Site.BLOCKED) { 207 return false; 208 } 209 var scopes = [Admin.REGULAR, Admin.TRUSTED]; 210 if (scopes.indexOf(status) < scopes.indexOf(scope)) { 211 return false; 212 } 213 if (!Membership.require(mode)) { 214 return false; 215 } 216 return true; 217 } 218 219 // Helper method for debugging 220 var renderMatrix = function() { 221 var buf = ['<table border=1 cellspacing=0>']; 222 for each (var scope in Admin.getNotificationScopes()) { 223 for each (var mode in Site.getNotificationModes()) { 224 for each (var status in Site.getStatus()) { 225 var perm = getPermission(scope.value, mode.value, status.value); 226 buf.push('<tr style="'); 227 perm && buf.push('color: blue;'); 228 if (scope.value === root.notificationScope && mode.value === 229 site.notificationMode && status.value === site.status) { 230 buf.push(' background-color: yellow;'); 231 } 232 buf.push('">'); 233 buf.push('<td>', scope.value, '</td>'); 234 buf.push('<td>', status.value, '</td>'); 235 buf.push('<td>', mode.value, '</td>'); 236 buf.push('<td>', perm, '</td>'); 237 buf.push('</tr>'); 238 } 239 } 240 } 241 buf.push('</table>'); 242 res.write(buf.join("")); 243 return; 244 } 245 246 switch (action) { 247 case "comment": 248 action = "create"; break; 249 } 250 251 var currentMembership = res.handlers.membership; 252 site.members.forEach(function() { 253 var membership = res.handlers.membership = this; 254 if (getPermission(root.notificationScope, site.notificationMode, site.status)) { 255 sendMail(membership.creator.email, gettext("Notification of changes at site {0}", 256 site.title), self.renderSkinAsString("$HopObject#notify_" + action)); 257 } 258 }); 259 res.handlers.membership = currentMembership; 260 return; 261 } 262 263 /** 264 * @returns {Tag[]} 265 */ 266 HopObject.prototype.getTags = function() { 267 var tags = []; 268 if (typeof this.tags === "object") { 269 this.tags.list().forEach(function(item) { 270 item.tag && tags.push(item.tag.name); 271 }); 272 } 273 return tags; 274 } 275 276 /** 277 * 278 * @param {Tag[]|String} tags 279 */ 280 HopObject.prototype.setTags = function(tags) { 281 if (typeof this.tags !== "object") { 282 return String.EMPTY; 283 } 284 285 if (!tags) { 286 tags = []; 287 } else if (tags.constructor === String) { 288 tags = tags.split(/\s*,\s*/); 289 } 290 291 var diff = {}; 292 var tag; 293 for (var i in tags) { 294 // Trim and remove troublesome characters (like ../.. etc.) 295 // We call getAccessName with a virgin HopObject to allow most names 296 tag = tags[i] = this.getAccessName.call(new HopObject, File.getName(tags[i])); 297 if (tag && diff[tag] == null) { 298 diff[tag] = 1; 299 } 300 } 301 this.tags.forEach(function() { 302 if (!this.tag) { 303 return; 304 } 305 diff[this.tag.name] = (tags.indexOf(this.tag.name) < 0) ? this : 0; 306 }); 307 308 for (var tag in diff) { 309 switch (diff[tag]) { 310 case 0: 311 // Do nothing (tag already exists) 312 break; 313 case 1: 314 // Add tag to story 315 this.addTag(tag); 316 break; 317 default: 318 // Remove tag 319 this.removeTag(diff[tag]); 320 } 321 } 322 return; 323 } 324 325 /** 326 * 327 * @param {String} name 328 */ 329 HopObject.prototype.addTag = function(name) { 330 this.tags.add(new TagHub(name, this, session.user)); 331 return; 332 } 333 334 /** 335 * 336 * @param {String} tag 337 */ 338 HopObject.prototype.removeTag = function(tag) { 339 var parent = tag._parent; 340 if (parent.size() === 1) { 341 parent.remove(); 342 } 343 tag.remove(); 344 return; 345 } 346 347 /** 348 * 349 * @param {Object} param 350 * @param {String} name 351 */ 352 HopObject.prototype.skin_macro = function(param, name) { 353 if (!name) { 354 return; 355 } 356 if (name.contains("#")) { 357 this.renderSkin(name); 358 } else { 359 var prototype = this._prototype || "Global"; 360 this.renderSkin(prototype + "#" + name); 361 } 362 return; 363 } 364 365 /** 366 * 367 * @param {Object} param 368 * @param {String} name 369 */ 370 HopObject.prototype.input_macro = function(param, name) { 371 param.name = name; 372 param.id = name; 373 param.value = this.getFormValue(name); 374 return html.input(param); 375 } 376 377 /** 378 * 379 * @param {Object} param 380 * @param {String} name 381 */ 382 HopObject.prototype.textarea_macro = function(param, name) { 383 param.name = name; 384 param.id = name; 385 param.value = this.getFormValue(name); 386 return html.textArea(param); 387 } 388 389 /** 390 * 391 * @param {Object} param 392 * @param {String} name 393 */ 394 HopObject.prototype.select_macro = function(param, name) { 395 param.name = name; 396 param.id = name; 397 var options = this.getFormOptions(name); 398 if (options.length < 2) { 399 param.disabled = "disabled"; 400 } 401 return html.dropDown(param, options, this.getFormValue(name)); 402 } 403 404 /** 405 * 406 * @param {Object} param 407 * @param {String} name 408 */ 409 HopObject.prototype.checkbox_macro = function(param, name) { 410 param.name = name; 411 param.id = name; 412 var options = this.getFormOptions(name); 413 if (options.length < 2) { 414 param.disabled = "disabled"; 415 } 416 param.value = String((options[1] || options[0]).value); 417 param.selectedValue = String(this.getFormValue(name)); 418 var label = param.label; 419 delete param.label; 420 html.checkBox(param); 421 if (label) { 422 html.element("label", label, {"for": name}); 423 } 424 return; 425 } 426 427 /** 428 * 429 * @param {Object} param 430 * @param {String} name 431 */ 432 HopObject.prototype.radiobutton_macro = function(param, name) { 433 param.name = name; 434 param.id = name; 435 var options = this.getFormOptions(name); 436 if (options.length < 2) { 437 param.disabled = "disabled"; 438 } 439 param.value = String(options[0].value); 440 param.selectedValue = String(this.getFormValue(name)); 441 var label = param.label; 442 delete param.label; 443 html.radioButton(param); 444 if (label) { 445 html.element("label", label, {"for": name}); 446 } 447 return; 448 } 449 450 /** 451 * 452 * @param {Object} param 453 * @param {String} name 454 */ 455 HopObject.prototype.upload_macro = function(param, name) { 456 param.name = name; 457 param.id = name; 458 param.value = this.getFormValue(name); 459 renderSkin("$Global#upload", param); 460 return; 461 } 462 463 /** 464 * 465 * @param {Object} param 466 * @param {HopObject} [handler] 467 */ 468 HopObject.prototype.macro_macro = function(param, handler) { 469 var ctor = this.constructor; 470 if ([Story, Image, File, Poll].indexOf(ctor) > -1) { 471 res.encode("<% "); 472 res.write(handler || ctor.name.toLowerCase()); 473 res.write(String.SPACE); 474 res.write(quote(this.name || this._id)); 475 res.encode(" %>"); 476 } 477 return; 478 } 479 480 /** 481 * 482 */ 483 HopObject.prototype.kind_macro = function() { 484 var type = this.constructor.name.toLowerCase(); 485 switch (type) { 486 default: 487 res.write(gettext(type)); 488 break; 489 } 490 return; 491 } 492 493 /** 494 * 495 * @param {String} name 496 * @returns {Number|String} 497 */ 498 HopObject.prototype.getFormValue = function(name) { 499 if (req.isPost()) { 500 return req.postParams[name]; 501 } else { 502 var value = this[name] || req.queryParams[name] || String.EMPTY; 503 return value instanceof HopObject ? value._id : value; 504 } 505 } 506 507 /** 508 * @returns {Object[]} 509 */ 510 HopObject.prototype.getFormOptions = function() { 511 return [{value: true, display: "enabled"}]; 512 } 513 514 /** 515 * @returns {HopObject} 516 */ 517 HopObject.prototype.self_macro = function() { 518 return this; 519 } 520 521 /** 522 * 523 */ 524 HopObject.prototype.type_macro = function() { 525 return res.write(this.constructor.name); 526 } 527 528 /** 529 * 530 * @param {Object} param 531 * @param {String} url 532 * @param {String} text 533 */ 534 HopObject.prototype.link_macro = function(param, url, text) { 535 url || (url = "."); 536 var action = url.split(/#|\?/)[0]; 537 if (this.getPermission(action)) { 538 if (!text) { 539 action === "." && (action = this._id); 540 text = gettext(action.capitalize()); 541 } 542 renderLink.call(global, param, url, text, this); 543 } 544 return; 545 } 546 547 /** 548 * 549 * @param {Object} param 550 * @param {String} format 551 */ 552 HopObject.prototype.created_macro = function(param, format) { 553 if (this.isPersistent()) { 554 format || (format = param.format); 555 res.write(formatDate(this.created, format)); 556 } 557 return; 558 } 559 560 /** 561 * 562 * @param {Object} param 563 * @param {String} format 564 */ 565 HopObject.prototype.modified_macro = function(param, format) { 566 if (this.isPersistent()) { 567 format || (format = param.format); 568 res.write(formatDate(this.modified, format)); 569 } 570 return; 571 } 572 573 /** 574 * 575 * @param {Object} param 576 * @param {String} mode 577 */ 578 HopObject.prototype.creator_macro = function(param, mode) { 579 if (!this.creator || this.isTransient()) { 580 return; 581 } 582 mode || (mode = param.as); 583 if (mode === "link" && this.creator.url) { 584 html.link({href: this.creator.url}, this.creator.name); 585 } else if (mode === "url") { 586 res.write(this.creator.url); 587 } else { 588 res.write(this.creator.name); 589 } return; 590 } 591 592 /** 593 * 594 * @param {Object} param 595 * @param {String} mode 596 */ 597 HopObject.prototype.modifier_macro = function(param, mode) { 598 if (!this.modifier || this.isTransient()) { 599 return; 600 } 601 mode || (mode = param.as); 602 if (mode === "link" && this.modifier.url) { 603 html.link({href: this.modifier.url}, this.modifier.name); 604 } else if (mode === "url") { 605 res.write(this.modifier.url); 606 } else { 607 res.write(this.modifier.name); 608 } 609 return; 610 } 611 612 /** 613 * @returns {String} 614 */ 615 HopObject.prototype.getTitle = function() { 616 return this.title || gettext(this.__name__.capitalize()); 617 } 618 619 /** 620 * @returns {String} 621 */ 622 HopObject.prototype.toString = function() { 623 return gettext(this.constructor.name) + " #" + this._id; 624 } 625 626 /** 627 * 628 * @param {String} text 629 * @param {Object} param 630 * @param {String} action 631 * @returns {String} 632 */ 633 HopObject.prototype.link_filter = function(text, param, action) { 634 action || (action = "."); 635 res.push(); 636 renderLink(param, action, text, this); 637 return res.pop(); 638 } 639 640 /** 641 * 642 * @param {String} name 643 */ 644 HopObject.prototype.handleMetadata = function(name) { 645 this.__defineGetter__(name, function() { 646 return this.metadata.get(name); 647 }); 648 this.__defineSetter__(name, function(value) { 649 return this.metadata.set(name, value); 650 }); 651 this[name + "_macro"] = function(param) { 652 var value; 653 if (value = this[name]) { 654 res.write(value); 655 } 656 return; 657 }; 658 return; 659 } 660