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