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 global variables and functions. 28 */ 29 30 app.addRepository(app.dir + "/../lib/rome-1.0.jar"); 31 app.addRepository(app.dir + "/../lib/jdom.jar"); 32 app.addRepository(app.dir + "/../lib/itunes-0.4.jar"); 33 34 app.addRepository("modules/core/Global.js"); 35 app.addRepository("modules/core/HopObject.js"); 36 app.addRepository("modules/core/Filters.js"); 37 app.addRepository("modules/core/JSON"); 38 app.addRepository("modules/core/Number.js"); 39 40 app.addRepository("modules/helma/File"); 41 app.addRepository("modules/helma/Image.js"); 42 app.addRepository("modules/helma/Html.js"); 43 app.addRepository("modules/helma/Http.js"); 44 app.addRepository("modules/helma/Mail.js"); 45 app.addRepository("modules/helma/Zip.js"); 46 47 app.addRepository("modules/jala/code/Date.js"); 48 app.addRepository("modules/jala/code/HopObject.js"); 49 app.addRepository("modules/jala/code/ListRenderer.js"); 50 app.addRepository("modules/jala/code/Utilities.js"); 51 52 // Adding i18n message files as repositories 53 (function() { 54 var dir = new helma.File(app.dir, "../i18n"); 55 for each (let fname in dir.list()) { 56 fname.endsWith(".js") && app.addRepository(app.dir + "/../i18n/" + fname); 57 } 58 })(); 59 // I18n.js needs to be added *after* the message files or the translations get lost 60 app.addRepository("modules/jala/code/I18n.js"); 61 62 // FIXME: Be careful with property names of app.data; 63 // they inherit all properties from HopObject! 64 /** 65 * Helma’s built-in application-wide in-memory store. 66 * @name app.data 67 * @namespace 68 */ 69 /** 70 * Temporary in-memory store of site callbacks. 71 * They will be invoked asynchronously by an Admin method. 72 * @see Admin.invokeCallbacks 73 * @see scheduler 74 * @name app.data.callbacks 75 */ 76 app.data.callbacks || (app.data.callbacks = []); 77 /** 78 * Temporary in-memory store of LogEntry instances. 79 * They will be made persistent asynchronously by an Admin method. 80 * @see Admin.commitEntries 81 * @see scheduler 82 * @name app.data.entries 83 * @type Array 84 */ 85 app.data.entries || (app.data.entries = []); 86 /** 87 * In-memory registry of Feature instances. 88 * Features are defined in the “extra” dir. 89 * @name app.data.features 90 * @type Array 91 */ 92 app.data.features || (app.data.features = []); 93 /** 94 * In-memory e-mail message queue. 95 * They will be sent asynchronously by an Admin method. 96 * @see helma.mail.flushQueue 97 * @see scheduler 98 * @name app.data.mails 99 * @type Array 100 */ 101 app.data.mails || (app.data.mails = []); 102 /** 103 * In-memory store of remote requests for counting story hits. 104 * They will be made persistent asynchronously by an Admin method. 105 * @see Admin.commitRequests 106 * @see scheduler 107 * @name app.data.requests 108 * @type Array 109 */ 110 app.data.requests || (app.data.requests = {}); 111 112 /** 113 * The helma.File prototype is defined as a module. 114 * @name helma.File 115 * @namespace 116 */ 117 /** 118 * Helper method for recursively copying a directory and its files. 119 * @param {helma.File} target 120 */ 121 helma.File.prototype.copyDirectory = function(target) { 122 /* 123 // Strange enough, Apache commons is not really faster... 124 var source = new java.io.File(this.toString()); 125 target = new java.io.File(target.toString()); 126 return Packages.org.apache.commons.io.FileUtils.copyDirectory(source, target); 127 */ 128 this.list().forEach(function(name) { 129 var file = new helma.File(this, name); 130 if (file.isDirectory()) { 131 file.copyDirectory(new helma.File(target, name)); 132 } else { 133 target.makeDirectory(); 134 file.hardCopy(new helma.File(target, name)); 135 } 136 }); 137 return; 138 } 139 140 /** 141 * The helma.Mail prototype is defined in a module. 142 * @name helma.Mail 143 * @namespace 144 */ 145 /** 146 * Add an e-mail message to the mail queue for later sending. 147 * @see app.data.mails 148 * @returns {Number} The number of mails waiting in the queue 149 */ 150 helma.Mail.prototype.queue = function() { 151 return app.data.mails.push(this); 152 } 153 154 /** 155 * Try to send and remove every mail instance collected in the mail queue. 156 * @see app.data.mails 157 */ 158 helma.Mail.flushQueue = function() { 159 if (app.data.mails.length > 0) { 160 app.debug("Flushing mail queue, sending " + 161 app.data.mails.length + " messages"); 162 var mail; 163 while (app.data.mails.length > 0) { 164 mail = app.data.mails.pop(); 165 mail.send(); 166 if (mail.status > 0) { 167 app.debug("Error while sending e-mail (status " + mail.status + ")"); 168 mail.writeToFile(getProperty("smtp.dir")); 169 } 170 } 171 } 172 return; 173 } 174 175 /** 176 * The jala.i18n namespace is defined in a module. 177 * @name jala.i18n 178 * @namespace 179 */ 180 jala.i18n.setLocaleGetter(function() { 181 return (res.handlers.site || root).getLocale(); 182 }); 183 184 /** 185 * The date format used in SQL queries and commands. 186 * @constant 187 * @type String 188 */ 189 var SQLDATEFORMAT = "yyyy-MM-dd HH:mm:ss"; 190 191 /** 192 * Regular Expression according to Jala’s HopObject.getAccessName(). 193 * @constant 194 * @type RegExp 195 */ 196 var NAMEPATTERN = /[\/+\\]/; 197 198 /** 199 * Shortcut for a function with empty body. 200 * Used e.g. in the disableMacro() method. 201 * @see disableMacro 202 * @function 203 */ 204 var idle = new Function; 205 206 /** 207 * An instance of Helma’s HTML rendering module. 208 * @type helma.Html 209 */ 210 var html = new helma.Html(); 211 212 /** 213 * An instance of the LESS parser. 214 * @type less.Parser 215 */ 216 var lessParser = new less.Parser(); 217 218 /** 219 * A collection of Java classes and namespaces required for parsing and generating RSS. 220 * @type Object 221 */ 222 var rome = new JavaImporter( 223 Packages.com.sun.syndication.io, 224 Packages.com.sun.syndication.feed.synd, 225 Packages.com.sun.syndication.feed.module.itunes, 226 Packages.com.sun.syndication.feed.module.itunes.types 227 ); 228 229 /** 230 * A simple and hackish implementation of the console instance of some browsers. 231 * @namespace 232 */ 233 var console = { 234 /** 235 * Convenience method for bridging log output from the server to the client. 236 * @methodOf console 237 * @param {String} text This text will be displayed in the browser’s console (if available). 238 */ 239 log: function(text) { 240 if (!res.meta.__console__) { 241 res.debug('<style>.helma-debug-line {border: none !important;}</style>'); 242 res.meta.__console__ = true; 243 } 244 var now = formatDate(new Date, Date.ISOFORMAT); 245 res.debug('<script>console.log("[Helma] ' + now + ' ===> ' + text + '")</script>'); 246 } 247 } 248 249 /** 250 * The startup handler Helma is calling automatically shortly after the application has started. 251 */ 252 function onStart() { 253 if (typeof root === "undefined") { 254 app.logger.error("Error in database configuration: no root site found."); 255 return; 256 } 257 // This is necessary once to be sure that aspect-oriented code will be applied 258 HopObject.prototype.onCodeUpdate && HopObject.prototype.onCodeUpdate(); 259 return; 260 } 261 262 /** 263 * This handler is called by Helma automatically before the application is stopped. 264 */ 265 function onStop() { /* Currently empty, just to avoid annoying log message */ } 266 267 /** 268 * Helper method to simultaneously define constants and a corresponding array of localized display names. 269 * @param {HopObject} ctor The desired prototype constructor the constants should be defined for. 270 * @returns {Function} 271 */ 272 function defineConstants(ctor /*, arguments */) { 273 var constants = [], name; 274 for (var i=1; i<arguments.length; i+=1) { 275 name = arguments[i].toUpperCase().replace(/\s/g, ""); 276 ctor[name] = arguments[i].toLowerCase(); 277 constants.push(arguments[i]); 278 } 279 return function() { 280 return constants.map(function(item) { 281 return { 282 value: item.toLowerCase(), 283 display: gettext(item) 284 } 285 }); 286 }; 287 } 288 289 /** 290 * Disables a macro with the idle() function. 291 * @see idle 292 * @param {HopObject} ctor The prototype constructor the macro is defined for 293 * @param {String} name The macro’s name 294 * @returns {Function} 295 */ 296 function disableMacro(ctor, name) { 297 return ctor.prototype[name + "_macro"] = idle; 298 } 299 300 /** 301 * Helma’s built-in main scheduler function. 302 * This method is regularly called by Helma after a defined period of time. 303 * The period is either defined by the return value or by the schedulerInterval property in app.properties. 304 * @returns {Number} The period in milliseconds the scheduler will be called again. 305 */ 306 function scheduler() { 307 helma.Mail.flushQueue(); 308 Admin.commitEntries(); 309 Admin.commitRequests(); 310 Admin.invokeCallbacks(); 311 Admin.updateDomains(); 312 Admin.updateHealth(); 313 Admin.purgeSites(); 314 return app.properties.schedulerInterval; 315 } 316 317 /** 318 * The nightly scheduler. 319 * This method is called according to the cron settings in app.properties. 320 */ 321 function nightly() { 322 var now = new Date; 323 if (now - (global.nightly.lastRun || -Infinity) < Date.ONEMINUTE) { 324 return; // Avoid running twice when main scheduler runs more than once per minute 325 } 326 app.log("***** Running nightly scripts *****"); 327 Admin.purgeReferrers(); 328 Admin.dequeue(); 329 global.nightly.lastRun = now; 330 return; 331 } 332 333 /** 334 * Renders a string depending on the comparison of two values. 335 * If the first value equals the second value, the first result will be returned; the second result otherwise. 336 * @example <% if <% macro %> is value then <% yes suffix=! %> else 'no :(' %>; 337 * Note that any value or result can be a macro, too. Thus, this can be used as 338 * a simple implementation of an if-then-else statement by using Helma macros only. 339 * @param {Object} param The default Helma macro parameter object 340 * @param {String} firstValue The first value 341 * @param {String} _is_ Syntactic sugar; should be "is" for legibility 342 * @param {String} secondValue The second value 343 * @param {String} _then_ Syntactic sugar; should be "then" for legibility 344 * @param {String} firstResult The first result, ie. the value that will be 345 * returned if the first value equals the second one 346 * @param {String} _else_ Syntactic sugar; should be "else" for legibility 347 * @param {String} secondResult The second result, ie. the value that will be 348 * returned if the first value does not equal the second one 349 * @returns {String} The resulting value 350 */ 351 function if_macro(param, firstValue, _is_, secondValue, _then_, firstResult, 352 _else_, secondResult) { 353 return (("" + firstValue) == ("" + secondValue)) ? firstResult : secondResult; 354 } 355 356 /** 357 * Renders the current date and time. 358 * @see formatDate 359 * @param {Object} param The default Helma macro parameter object 360 * @param {String} [format] A date format string 361 * @returns {String} The formatted current date string 362 */ 363 function now_macro(param, format) { 364 return formatDate(new Date, format || param.format); 365 } 366 367 /** 368 * Renders a link. 369 * @see renderLink 370 * @returns {String} The rendered HTML link element 371 */ 372 function link_macro() { 373 return renderLink.apply(this, arguments); 374 } 375 376 /** 377 * Renders a skin from within a skin. 378 * @see HopObject#skin_macro 379 * @returns {String} The rendered skin 380 */ 381 // FIXME: The definition with "var" is necessary; otherwise the skin_macro() 382 // method won't be overwritten reliably. (Looks like a Helma bug.) 383 var skin_macro = function(param, name) { 384 return HopObject.prototype.skin_macro.apply(this, arguments); 385 } 386 387 /** 388 * Renders a breadcrumbs navigation from the current HopObject path. 389 * @param {Object} param The default Helma macro parameter object 390 * @param {String} [delimiter=' : '] The string visually separating two navigation items 391 */ 392 function breadcrumbs_macro (param, delimiter) { 393 delimiter || (delimiter = param.separator || " : "); 394 //html.link({href: res.handlers.site.href()}, res.handlers.site.getTitle()); 395 var offset = res.handlers.site === root ? 1 : 2; 396 for (var item, title, i=offset; i<path.length; i+=1) { 397 if (item = path[i]) { 398 if (!isNaN(item._id) && item.constructor !== Layout) { 399 continue; 400 } 401 if (i === path.length-1 && req.action === "main") { 402 res.write(item.getTitle()); 403 } else { 404 html.link({href: path[i].href()}, item.getTitle()); 405 } 406 (i < path.length-1) && res.write(delimiter); 407 } 408 } 409 if (req.action !== "main") { 410 res.write(delimiter); 411 res.write(gettext(req.action.titleize())); 412 } 413 return; 414 } 415 416 /** 417 * Helper macro for checking if a user session is authenticated (logged in). 418 * Returns true if user is logged in, false otherwise. 419 * @returns {Boolean} 420 */ 421 function user_macro() { 422 return !!session.user; 423 } 424 425 /** 426 * Renders the URL of, a link to or an arbitrary skin of a story. 427 * @param {Object} param The default Helma macro parameter object 428 * @param {String} [param.skin='embed'] The name of a story skin 429 * @param {String} id The id or path of the desired story 430 * @param {String} [mode] Either of 'url' or 'link' 431 * @example <% story 1810 skin=preview %> Story #1810 in preview skin 432 * <% story blog/1971 url %> URL of the story of site “blog” 433 */ 434 function story_macro(param, id, mode) { 435 var story = HopObject.getFromPath(id, "stories"); 436 if (!story || !story.getPermission("main")) { 437 return; 438 } 439 440 switch (mode) { 441 case "url": 442 res.write(story.href()); 443 break; 444 case "link": 445 html.link({href: story.href()}, story.getTitle()); 446 break; 447 default: 448 story.renderSkin("Story#" + (param.skin || "embed")); 449 } 450 return; 451 } 452 453 /** 454 * Renders the URL or an arbitrary skin of a file. 455 * @param {Object} param The default Helma macro parameter object 456 * @param {String} [param.skin='main'] The name of a file skin 457 * @param {String} id The id or path of the desired file 458 * @param {String} [mode] Currently only possible value is 'url' 459 * @example <% file 1810 url %> URL of file #1810 460 * <% file blog/text.pdf skin=preview %> File in site “blog” using preview skin 461 * <% file /image.raw %> Static file of root site 462 */ 463 function file_macro(param, id, mode) { 464 if (!id) { 465 return; 466 } 467 468 var file; 469 if (id.startsWith("/")) { 470 name = id.substring(1); 471 if (mode === "url") { 472 res.write(root.getStaticUrl(name)); 473 } else { 474 file = root.getStaticFile(name); 475 res.push(); 476 File.prototype.contentLength_macro.call({ 477 contentLength: file.getLength() 478 }); 479 res.handlers.file = { 480 href: root.getStaticUrl(name), 481 name: name, 482 contentLength: res.pop() 483 }; 484 File.prototype.renderSkin("File#main"); 485 } 486 return; 487 } 488 489 file = HopObject.getFromPath(id, "files"); 490 if (!file) { 491 return; 492 } 493 if (mode === "url") { 494 res.write(file.getUrl()); 495 } else { 496 file.renderSkin("File#" + (param.skin || "main")); 497 } 498 return; 499 } 500 501 /** 502 * Renders the URL, a thumbnail or an HTML element of an image. 503 * @see Image#thumbnail_macro 504 * @see Image#render_macro 505 * @param {Object} param The default Helma macro parameter object 506 * @param {String} id The id or path of the desired image 507 * @param {String} [mode] Either of 'url' or 'thumbnail' 508 */ 509 function image_macro(param, id, mode) { 510 if (!id) { 511 return; 512 } 513 514 var image; 515 if (id.startsWith("/")) { 516 var name = id.substring(1); 517 image = Images.Default[name] || Images.Default[name + ".gif"]; 518 } else { 519 image = HopObject.getFromPath(id, "images"); 520 // FIXME: Could fallback be replaced with CSS background-image? 521 if (!image && param.fallback) { 522 image = HopObject.getFromPath(param.fallback, "images"); 523 } 524 } 525 526 if (!image) { 527 return; 528 } 529 530 switch (mode) { 531 case "url": 532 res.write(image.getUrl()); 533 break; 534 case "thumbnail": 535 case "popup": 536 var url = image.getUrl(); 537 html.openTag("a", {href: url}); 538 // FIXME: Bloody popups belong to compatibility layer 539 (mode === "popup") && (param.onclick = 'javascript:openPopup(\'' + 540 url + '\', ' + image.width + ', ' + image.height + '); return false;') 541 image.thumbnail_macro(param); 542 html.closeTag("a"); 543 break; 544 default: 545 image.render_macro(param); 546 } 547 return; 548 } 549 550 /** 551 * Renders the URL, a link or the visual representation of a poll. 552 * @param {Object} param The default Helma macro parameter object 553 * @param {String} id The id or path of the desired poll 554 * @param {String} mode Either of 'url' or 'link' 555 */ 556 function poll_macro(param, id, mode) { 557 if (!id) { 558 return; 559 } 560 561 var poll = HopObject.getFromPath(id, "polls"); 562 if (!poll) { 563 return; 564 } 565 566 switch (mode) { 567 case "url": 568 res.write(poll.href()); 569 break; 570 case "link": 571 html.link({ 572 href: poll.href(poll.status === Poll.CLOSED ? "result" : "") 573 }, poll.question); 574 break; 575 default: 576 if (poll.status === Poll.CLOSED || mode === "results") 577 poll.renderSkin("$Poll#results", {}); 578 else { 579 poll.renderSkin("$Poll#main", {}); 580 } 581 } 582 return; 583 } 584 585 /** 586 * The “swiss army knife” list macro. Lists collections of HopObjects. 587 * There is hardly a thing it cannot do… but it’s kind of messy, though. 588 * @param {Object} param The default Helma macro parameter object 589 * @param {String} [param.skin=preview] The name of a skin suitable for the collection 590 * @param {String} id The identifier of the desired collection 591 * @param {Number} [limit=25] The maximum amount of items listed 592 * @example <% list sites %> 593 * <% list updates 10 %> 594 * <% list blog/comments %> 595 * <% list featured skin=promotion %> 596 * <% list images %> 597 * <% list postings %> 598 * <% list stories %> 599 * <% list tags %> 600 */ 601 function list_macro(param, id, limit) { 602 if (!id) { 603 return; 604 } 605 606 var max = Math.min(limit || 25, 50); 607 var collection, skin; 608 if (id === "sites") { 609 collection = root.sites.list(0, max); 610 skin = "Site#preview"; 611 } else if (id === "updates") { 612 collection = root.updates.list(0, limit); 613 skin = "Site#preview"; 614 } else { 615 var site; 616 var parts = id.split("/"); 617 if (parts.length > 1) { 618 type = parts[1]; 619 site = root.sites.get(parts[0]); 620 } else { 621 type = parts[0]; 622 } 623 624 site || (site = res.handlers.site); 625 var filter = function(item, index) { 626 return index < max && item.getPermission("main"); 627 } 628 629 var commentFilter = function(item) { 630 if (item.story.status !== Story.CLOSED && 631 item.site.commentMode !== Site.DISABLED && 632 item.story.commentMode !== Story.CLOSED) { 633 return true; 634 } 635 return false; 636 } 637 638 switch (type) { 639 case "comments": 640 if (site.commentMode !== Site.DISABLED) { 641 var comments = site.stories.comments; 642 collection = comments.list().filter(filter); 643 skin = "Story#preview"; 644 } 645 break; 646 647 case "featured": 648 collection = site.stories.featured.list(0, max); 649 prototype = "Story#preview"; 650 break; 651 652 case "images": 653 collection = site.images.list(0, max); 654 prototype = "Image#preview"; 655 break; 656 657 case "postings": 658 content = site.stories.union; 659 collection = content.list().filter(filter).filter(function(item) { 660 if (item.constructor === Comment) { 661 return commentFilter(item); 662 } 663 return true; 664 }); 665 prototype = "Story#preview"; 666 break; 667 668 case "stories": 669 var stories = site.stories.recent; 670 var counter = 0; 671 collection = stories.list().filter(function(item, index) { 672 return item.constructor === Story && filter(item, counter++); 673 }); 674 prototype = "Story#preview"; 675 break; 676 677 case "tags": 678 return site.tags.list_macro(param, param.skin || "$Tag#preview"); 679 break; 680 681 default: 682 break; 683 } 684 } 685 for each (var item in collection) { 686 item.renderSkin(param.skin || skin); 687 } 688 return; 689 } 690 691 /** 692 * Defines and renders a value. 693 * This works like a variable that can be set in one skin and rendered in another – 694 * which must be rendered later than the one setting the variable. 695 * @param {Object} param The default Helma macro parameter object. 696 * @param {String} name The name of the value. 697 * @param {String} [value] The desired value. 698 * If no value is given, the current value will be rendered. 699 * @example <% value foo=bar %> Defines res.meta.values.foo = bar 700 * @example <% value foo %> Renders the value of res.meta.value.foo 701 */ 702 function value_macro(param, name, value) { 703 if (!name) { 704 return; 705 } 706 name = name.toLowerCase(); 707 if (!value) { 708 res.write(res.meta.values[name]); 709 } else { 710 //res.write("/* set " + name + " to " + value + " */"); 711 res.meta.values[name] = value; 712 } 713 return; 714 } 715 716 /** 717 * Renders either a skin or the URL of a random site, story or image. 718 * The corresponding story and image collections will be retrieved either from res.handlers.site or 719 * from the prefixed “type” argument (e.g. “mySite/story”). 720 * Furthermore, both collections can be reduced to a specific tag or gallery, resp. 721 * @param {Object} param The default Helma macro parameter object. 722 * @param {String} [param.skin = "preview"] The name of the skin to render in default output mode. 723 * @param {String} [param.tag] Reduce the story collection to stories with the specified tag. 724 * @param {String} [param.gallery] Reduce the image collection to images from the specified gallery. 725 * @param {String} type The type of object to render. Either of “site”, “story” or “image”. 726 * It can be prepended by a site name delimited by a slash: “mySite/image”. 727 * @param {String} [mode] Set the output mode. Currently, only “url” is supported. 728 * @example <% random site skin=preview %> Renders the preview skin of a random site. 729 * <% random story tag=essay url %> Renders the URL of a random story tagged with “essay”. 730 * <% random foo/image gallery=cat %> Renders the default skin of a random image in the gallery “cat“ of the site “foo”. 731 */ 732 function random_macro(param, type, mode) { 733 var getRandom = function(n) { 734 return Math.floor(Math.random() * n); 735 }; 736 737 var site = res.handlers.site; 738 739 if (type === "site") { 740 site = root.sites.get(getRandom(root.sites.size())); 741 mode === 'url' ? res.write(site.href()) : 742 site.renderSkin(param.skin || "Site#preview"); 743 return; 744 } 745 746 var parts = type.split("/"); 747 if (parts.length > 1) { 748 site = root.sites.get(parts[0] || 'www'); 749 type = parts[1]; 750 } else { 751 type = parts[0]; 752 } 753 754 if (!site) { 755 return; 756 } 757 758 switch (type) { 759 case "story": 760 case "stories": 761 var stories = param.tag ? site.stories.tags.get(param.tag) : 762 site.stories.featured; 763 var story = stories && stories.get(getRandom(stories.size())); 764 if (story) { 765 param.tag && (story = story.tagged); 766 mode === 'url' ? res.write(story.href()) : 767 story.renderSkin(param.skin || "Story#preview"); 768 } 769 break; 770 771 case "image": 772 case "images": 773 var images = param.gallery ? site.images.galleries.get(param.gallery) : 774 site.images; 775 var image = images && images.get(getRandom(images.size())); 776 if (image) { 777 param.gallery && (image = image.tagged); 778 mode === 'url' ? res.write(image.href()) : 779 image.renderSkin(param.skin || "Image#preview"); 780 } 781 break; 782 } 783 return; 784 } 785 786 /** 787 * Renders the Antville version string. 788 * @param {Object} param The default Helma macro parameter object. 789 * @param {String} [type = 'default'] The type of version string. 790 * @see Root.VERSION 791 */ 792 function version_macro(param, type) { 793 var version = Root.VERSION; 794 var result = version[type || "default"]; 795 return result || version; 796 } 797 798 /** 799 * Renders a string vertically in the global listItemFlag skin. 800 * @param {Object} param The default Helma macro parameter object. 801 * @param {String} str The string to be rendered. 802 */ 803 function listItemFlag_macro(param, str) { 804 res.push(); 805 for (var i=0; i<str.length; i+=1) { 806 res.write(str.charAt(i)); 807 res.write("<br />"); 808 } 809 renderSkin("$Global#listItemFlag", {text: res.pop()}); 810 return; 811 } 812 813 814 /** 815 * A simple Helma macro filter returning one of two possible values depending on which one is truthy. 816 * @param {Object} value The original (desired) value. 817 * @param {Object} param The default Helma macro parameter object. 818 * @param {Object} defaultValue The fallback value for use if the original value should be untruthy. 819 * @returns {Object} The value argument if truthy, the defaultValue argument otherwise. 820 */ 821 function default_filter(value, param, defaultValue) { 822 return value || defaultValue; 823 } 824 825 /** 826 * Helma macro filter wrapping the {@link Date#getAge} method. 827 * @see Date#getAge 828 * @param {Date} value The original date. 829 * @param {Object} param The default Helma macro parameter object. 830 * @returns {String} The resulting age string of the original date. 831 */ 832 function age_filter(value, param) { 833 if (!value || value.constructor !== Date) { 834 return value; 835 } 836 return value.getAge() 837 } 838 839 /** 840 * Helma macro filter wrapping the {@link renderLink} method. 841 * @param {String} text The link text. 842 * @param {String} param The default Helma macro parameter object. 843 * @param {Object} [url = text] The link URL. 844 * @returns {String} The rendered link element 845 * @see renderLink 846 */ 847 function link_filter(text, param, url) { 848 if (text) { 849 url || (url = text); 850 res.push(); 851 renderLink(param, url, text); 852 return res.pop(); 853 } 854 return; 855 } 856 857 /** 858 * Helma macro filter wrapping the global formatting methods. 859 * @see formatNumber 860 * @see formatDate 861 * @param {Object} value The original value. 862 * @param {Object} param The default Helma macro parameter object. 863 * @param {String} pattern A formatting pattern suitable for the formatting method. 864 * @param {String} [type] Deprecated. 865 * @returns {String} The formatted string. 866 */ 867 function format_filter(value, param, pattern, type) { 868 if (!value && value !== 0) { 869 return; 870 } 871 var f = global["format" + value.constructor.name]; 872 if (f && f.constructor === Function) { 873 return f(value, pattern || param.pattern, type); 874 } 875 return value; 876 } 877 878 /** 879 * Macro filter for clipping output. 880 * @param {String} input The original input. 881 * @param {Object} param The default Helma macro parameter object. 882 * @param {Number} [limit = 20] The maximum amount of text parts to be displayed. 883 * @param {String} [clipping = '...'] The replacement for the clipped portions of the text. 884 * @param {String} [delimiter = '\\s'] The regular expression string used to split the text into parts. 885 * @returns {String} The clipped result. 886 */ 887 function clip_filter(input, param, limit, clipping, delimiter) { 888 var len = 0; 889 if (input) { 890 len = input.length; 891 input = input.stripTags(); 892 } 893 input || (input = ngettext("({0} character)", "({0} characters)", len)); 894 limit || (limit = 20); 895 clipping || (clipping = "..."); 896 delimiter || (delimiter = "\\s"); 897 return String(input || "").head(limit, clipping, delimiter); 898 } 899 900 /** 901 * Renders an HTML <a> element from a URL or HopObject. 902 * @see helma.Html#link 903 * @see HopObject#link_macro 904 * @param {Object} param The default Helma macro parameter object. 905 * @param {String} [param.title] An optional link title for use in the “title” attribute. 906 * @param {String} url A complete or partial URL string. Optional if “handler” is specified. 907 * @param {String} [text] An optional link text. 908 * @param {HopObject} handler The HopObject used as base URL. Optional if “url” is specified. 909 */ 910 function renderLink(param, url, text, handler) { 911 url || (url = param.url || String.EMPTY); 912 text || (text = param.text || url); 913 if (!text || (handler && !handler.href)) { 914 return; 915 } 916 if (url === "." || url === "main") { 917 url = String.EMPTY; 918 } 919 delete param.url; 920 delete param.text; 921 param.title || (param.title = String.EMPTY); 922 if (!handler || url.contains(":")) { 923 param.href = url; 924 } else if (url.contains("/") || url.contains("?") || url.contains("#")) { 925 var parts = url.split(/(\/|\?|#)/); 926 param.href = handler.href(parts[0]) + parts.splice(1).join(String.EMPTY); 927 } else { 928 param.href = handler.href(url); 929 } 930 html.link(param, text); 931 return; 932 } 933 934 /** 935 * Validates if a string is suitable for e-mail messaging. 936 * @see String#isEmail 937 * @param {String} str The string to be validated. 938 * @returns {String|null} The e-mail string if valid, null otherwise. 939 */ 940 function validateEmail(str) { 941 if (str) { 942 if (str.isEmail()) { 943 return str; 944 } 945 } 946 return null; 947 } 948 949 /** 950 * Validates if a string is suitable for requesting a URL. 951 * @param {String} str The string to be validated. 952 * @returns {String|null} The URL string if valid, null otherwise. 953 */ 954 function validateUrl(str) { 955 if (str) { 956 if (str.isUrl()) { 957 return str; 958 } else if (str.isEmail()) { 959 return "mailto:" + str; 960 } else { 961 return null; 962 } 963 } 964 return null; 965 } 966 967 /** 968 * Surrounds a string by programmer quotes ("). 969 * @param {String} str The original string. 970 * @returns {String} The processed string. 971 */ 972 function quote(str) { 973 if (/[\W\D]/.test(str)) { 974 str = '"' + str + '"'; 975 } 976 return str; 977 } 978 979 /** 980 * Formats a number according to a pattern and the site’s locale setting. 981 * @param {Number} number The original number. 982 * @param {String} pattern The formatting pattern. 983 * @returns {String} The formatted number string. 984 */ 985 function formatNumber(number, pattern) { 986 return Number(number).format(pattern, res.handlers.site.getLocale()); 987 } 988 989 /** 990 * Formats a date according to a formatting string and the site’s locale and time zone. 991 * @param {Date} date The original date. 992 * @param {String} [format = "full"] The formatting string. Either a {@link http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html Java SimpleDateFormat pattern} or of “short”, “medium”, “long”, “full”, “date”, “time”, “iso” or “text”. 993 * @returns {String} The formatted date string. 994 */ 995 function formatDate(date, format) { 996 if (!date) { 997 return null; 998 } 999 1000 var pattern, 1001 site = res.handlers.site, 1002 locale = site.getLocale(); 1003 1004 switch (format) { 1005 case null: 1006 case undefined: 1007 format = "full"; // Caution! Passing through to next case block! 1008 case "short": 1009 case "medium": 1010 case "long": 1011 case "full": 1012 var type = java.text.DateFormat[format.toUpperCase()]; 1013 pattern = java.text.DateFormat.getDateTimeInstance(type, type, locale).toPattern(); 1014 break; 1015 1016 case "date": 1017 var type = java.text.DateFormat.FULL 1018 pattern = java.text.DateFormat.getDateInstance(type, locale).toPattern(); 1019 break; 1020 1021 case "time": 1022 var type = java.text.DateFormat.SHORT; 1023 pattern = java.text.DateFormat.getTimeInstance(type, locale).toPattern(); 1024 break; 1025 1026 case "iso": 1027 pattern = Date.ISOFORMAT; 1028 break; 1029 1030 case "text": 1031 var text, 1032 now = new Date, 1033 diff = now - date; 1034 if (diff < 0) { 1035 // FIXME: Do something similar for future dates 1036 text = formatDate(date); 1037 } else if (diff < Date.ONEMINUTE) { 1038 text = gettext("Right now"); 1039 } else if (diff < Date.ONEHOUR) { 1040 text = ngettext("{0} minute ago", "{0} minutes ago", 1041 parseInt(diff / Date.ONEMINUTE, 10)); 1042 } else if (diff < Date.ONEDAY) { 1043 text = ngettext("{0} hour ago", "{0} hours ago", 1044 parseInt(diff / Date.ONEHOUR, 10)); 1045 } else if (diff < 2 * Date.ONEDAY) { 1046 text = gettext("Yesterday"); 1047 } else { 1048 text = ngettext("{0} day ago", "{0} days ago", 1049 parseInt(diff / Date.ONEDAY, 10)); 1050 } 1051 return text; 1052 1053 default: 1054 pattern = format; 1055 } 1056 1057 try { 1058 return date.format(pattern, locale, site.getTimeZone()); 1059 } catch (ex) { 1060 return "[Invalid date format]"; 1061 } 1062 1063 return String.EMPTY; 1064 } 1065 1066 /** 1067 * Injects the XSLT stylesheet declaration into an XML string until Mozilla developers will have mercy. 1068 * @param {String} xml An XML string 1069 * @returns {String} An XML string containing the XSLT stylesheet declaration 1070 */ 1071 function injectXslDeclaration(xml) { 1072 res.push(); 1073 renderSkin("Global#xslDeclaration"); 1074 return xml.replace(/(\?>\r?\n?)/, "$1" + res.pop()); 1075 } 1076 1077 /** 1078 * General mail sending function. Mails will be queued in app.data.mails. 1079 * @param {Object} recipient The recipient's email addresses 1080 * @param {String} subject The e-mail's subject 1081 * @param {String} body The body text of the e-mail 1082 * @returns {Number} The status code of the underlying helma.Mail instance 1083 */ 1084 function sendMail(recipient, subject, body, options) { 1085 options || (options = {}); 1086 if (!recipient || !body) { 1087 throw Error("Insufficient arguments in method sendMail()"); 1088 } 1089 var mail = new helma.Mail(getProperty("smtp", "localhost"), 1090 getProperty("smtp.port", "25")); 1091 mail.setFrom(root.replyTo || "root@localhost"); 1092 if (recipient instanceof Array) { 1093 for (var i in recipient) { 1094 mail.addBCC(recipient[i]); 1095 } 1096 } else { 1097 mail.addTo(recipient); 1098 } 1099 mail.setSubject(subject); 1100 mail.setText(body); 1101 if (options.footer !== false) { // It is the exception to have no footer 1102 mail.addText(renderSkinAsString("$Global#mailFooter")); 1103 } 1104 mail.queue(); 1105 return mail.status; 1106 } 1107 1108 /** 1109 * Retrieves the locale object from a language string. 1110 * @param {String} language The name of the language. 1111 * @returns {java.util.Locale} The corresponding locale object. 1112 */ 1113 function getLocale(language) { 1114 return new java.util.Locale(language || "english"); 1115 } 1116 1117 /** 1118 * Creates an array of all available Java locales sorted by their names. 1119 * @param {String} language The optional language of the locales 1120 * @returns {Object[]} A sorted array containing the corresponding locales 1121 */ 1122 function getLocales(language) { 1123 var result = [], locale, localeString; 1124 var locales = java.util.Locale.getAvailableLocales(); 1125 for (var i in locales) { 1126 locale = locales[i]; 1127 localeString = locale.toString(); 1128 if (!localeString.contains("_")) { 1129 result.push({ 1130 value: localeString, 1131 display: locale.getDisplayName(locale), 1132 "class": jala.i18n.getCatalog(jala.i18n.getLocale(localeString)) ? "translated" : "" 1133 }); 1134 } 1135 } 1136 result.sort(new String.Sorter("display")); 1137 return result; 1138 } 1139 1140 /** 1141 * This method returns an array of structs providing two properties each: 1142 * <code>value</code> – a unique time zone ID 1143 * <code>display</code> – a (more) user-friendly string 1144 * Although Java is great in providing all time zones one can imagine, this 1145 * vast amount of choices fails to support easy time zone selection. 1146 * Furthermore, the L10n features of the java.util.TimeZone class are insufficient 1147 * as they do only translate the generic string returned by the getDisplayName() 1148 * method (e.g. Central European Time), not the more usable time zone IDs 1149 * (e.g. Europe/Vienna). Thus, time zone selection in Antville is rather limited. 1150 * @param {String} language 1151 * @returns {Object[]} A sorted array containing the corresponding timezones 1152 */ 1153 function getTimeZones(language) { 1154 var result = [], 1155 timeZones = [], 1156 locale = getLocale(language), 1157 ids = java.util.TimeZone.getAvailableIDs(); 1158 1159 for each (let id in ids) { 1160 // Exclude confusing time zones 1161 if (id.length < 4 || !id.contains("/") || 1162 id.startsWith("Etc") || id.startsWith("System")) { 1163 continue; 1164 } 1165 let timeZone = java.util.TimeZone.getTimeZone(id); 1166 // Exclude more confusing time zones 1167 if (timeZone.getDisplayName().startsWith("GMT")) { 1168 continue; 1169 } 1170 result.push({ 1171 value: timeZone.getID(), 1172 display: timeZone.getID().replace(/_/g, String.SPACE) 1173 }) 1174 timeZones.push(timeZone); 1175 } 1176 1177 return result.sort(new String.Sorter("display")); 1178 } 1179 // FIXME: 1180 /** 1181 * Replaces <img> elements in a string with <a> elements to fix RSS output which is not capable of displaying images. 1182 * @param {String} rss The original RSS output. 1183 * @returns {String} The transformed RSS output. 1184 */ 1185 function fixRssText(rss) { 1186 var re = new RegExp("<img src\\s*=\\s*\"?([^\\s\"]+)?\"?[^>]*?(alt\\s*=\\s*\"?([^\"]+)?\"?[^>]*?)?>", "gi"); 1187 rss = rss.replace(re, "[<a href=\"$1\" title=\"$3\">Image</a>]"); 1188 return rss; 1189 } 1190 1191 // FIXME: 1192 /** 1193 * @ignore 1194 * @param {Object} src 1195 */ 1196 function doWikiStuff (src) { 1197 // robert, disabled: didn't get the reason for this: 1198 // var src= " "+src; 1199 if (src == null || !src.contains("<*")) 1200 return src; 1201 1202 // do the Wiki link thing, <*asterisk style*> 1203 var regex = new RegExp ("<[*]([^*]+)[*]>"); 1204 regex.ignoreCase=true; 1205 1206 var text = ""; 1207 var start = 0; 1208 while (true) { 1209 var found = regex.exec (src.substring(start)); 1210 var to = found == null ? src.length : start + found.index; 1211 text += src.substring(start, to); 1212 if (found == null) 1213 break; 1214 var name = ""+(new java.lang.String (found[1])).trim(); 1215 var item = res.handlers.site.topics.get (name); 1216 if (item == null && name.lastIndexOf("s") == name.length-1) 1217 item = res.handlers.site.topics.get (name.substring(0, name.length-1)); 1218 if (item == null || !item.size()) 1219 text += format(name)+" <small>[<a href=\""+res.handlers.site.stories.href("create")+"?topic="+escape(name)+"\">define "+format(name)+"</a>]</small>"; 1220 else 1221 text += "<a href=\""+item.href()+"\">"+name+"</a>"; 1222 start += found.index + found[1].length+4; 1223 } 1224 return text; 1225 } 1226 1227 // FIXME: Rewrite with jala.ListRenderer? 1228 /** 1229 * Renders an HTML list from a HopObject collection or an array. 1230 * @param {HopObject|Array} collection The original collection of objects. 1231 * @param {Function|Skin} funcOrSkin A skin name or a rendering function. 1232 * @param {Number} itemsPerPage The amount of rendered items per page. 1233 * @param {Number} pageIdx The current page index. 1234 * @returns {String} The rendered list. 1235 */ 1236 function renderList(collection, funcOrSkin, itemsPerPage, pageIdx) { 1237 var currIdx = 0, item; 1238 var isArray = collection instanceof Array; 1239 var stop = size = isArray ? collection.length : collection.size(); 1240 1241 if (itemsPerPage) { 1242 var totalPages = Math.ceil(size/itemsPerPage); 1243 if (isNaN(pageIdx) || pageIdx > totalPages || pageIdx < 0) { 1244 pageIdx = 0; 1245 } 1246 currIdx = pageIdx * itemsPerPage; 1247 stop = Math.min(currIdx + itemsPerPage, size); 1248 } 1249 1250 var isFunction = (funcOrSkin instanceof Function) ? true : false; 1251 res.push(); 1252 while (currIdx < stop) { 1253 item = isArray ? collection[currIdx] : collection.get(currIdx); 1254 isFunction ? funcOrSkin(item) : item.renderSkin(funcOrSkin); 1255 currIdx += 1; 1256 } 1257 return res.pop(); 1258 } 1259 1260 // FIXME: Rewrite using jala.ListRenderer or rename (eg. renderIndex) 1261 /** 1262 * Renders the page navigation for a collection of HopObjects. 1263 * @param {HopObject|Array|Number} collectionOrSize A collection or just the size of a collection. 1264 * @param {String} url The base URL for rendering links. 1265 * @param {Number} itemsPerPage The amount of rendered items per page. 1266 * @param {Number} pageIdx The current page index. 1267 * @returns {String} The rendered page navigation. 1268 */ 1269 function renderPager(collectionOrSize, url, itemsPerPage, pageIdx) { 1270 // Render a single item for the navigation bar 1271 var renderItem = function(text, cssClass, url, page) { 1272 var param = {"class": cssClass}; 1273 if (!url) { 1274 param.text = text; 1275 } else { 1276 if (url.contains("?")) 1277 param.text = html.linkAsString({href: url + "&page=" + page}, text); 1278 else 1279 param.text = html.linkAsString({href: url + "?page=" + page}, text); 1280 } 1281 renderSkin("$Global#pagerItem", param); 1282 return; 1283 } 1284 1285 var maxItems = 10; 1286 var size = 0; 1287 if (collectionOrSize instanceof Array) { 1288 size = collectionOrSize.length; 1289 } else if (collectionOrSize instanceof HopObject) { 1290 size = collectionOrSize.size(); 1291 } else if (!isNaN(collectionOrSize)) { 1292 size = parseInt(collectionOrSize, 10); 1293 } 1294 var lastPageIdx = Math.ceil(size/itemsPerPage)-1; 1295 // If there's just one page no navigation will be rendered 1296 if (lastPageIdx <= 0) { 1297 return null; 1298 } 1299 1300 // Initialize the parameter object 1301 var param = {}; 1302 var pageIdx = parseInt(pageIdx, 10); 1303 // Check if the passed index is correct 1304 if (isNaN(pageIdx) || pageIdx > lastPageIdx || pageIdx < 0) { 1305 pageIdx = 0; 1306 } 1307 param.display = ((pageIdx * itemsPerPage) + 1) + "-" + 1308 (Math.min((pageIdx * itemsPerPage) + itemsPerPage, size)); 1309 param.total = size; 1310 1311 // Render the navigation-bar 1312 res.push(); 1313 (pageIdx > 0) && renderItem("[–]", "pageNavItem", url, pageIdx-1); 1314 var offset = Math.floor(pageIdx / maxItems) * maxItems; 1315 (offset > 0) && renderItem("[..]", "pageNavItem", url, offset-1); 1316 var currPage = offset; 1317 var stop = Math.min(currPage + maxItems, lastPageIdx+1); 1318 while (currPage < stop) { 1319 if (currPage === pageIdx) { 1320 renderItem("[" + (currPage +1) + "]", "pageNavSelItem"); 1321 } else { 1322 renderItem("[" + (currPage +1) + "]", "pageNavItem", url, currPage); 1323 } 1324 currPage += 1; 1325 } 1326 if (currPage < lastPageIdx) { 1327 renderItem("[..]", "pageNavItem", url, offset + maxItems); 1328 } 1329 if (pageIdx < lastPageIdx) { 1330 renderItem("[+]", "pageNavItem", url, pageIdx +1); 1331 } 1332 param.pager = res.pop(); 1333 return renderSkinAsString("$Global#pager", param); 1334 } 1335 1336 /** 1337 * Transforms an english plural form of a noun into its singular form. 1338 * @param {String} plural The noun in plural form. 1339 * @returns {String} The english singular form of the original input. 1340 */ 1341 function singularize(plural) { 1342 if (plural.endsWith("ies")) { 1343 return plural.substring(0, plural.length-3) + "y"; 1344 } 1345 return plural.substring(0, plural.length-1); 1346 } 1347 1348 /** 1349 * Transforms an english singular form of a noun into its plural form. 1350 * @param {String} singular The noun in singular form. 1351 * @returns {String} The english plural form of the original input. 1352 */ 1353 function pluralize(singular) { 1354 if (singular.endsWith("y")) { 1355 return singular.substring(0, singular.length-1) + "ies"; 1356 } 1357 return singular + "s"; 1358 } 1359 1360 /** 1361 * Halts the execution of the thread for the specified amount of milliseconds. 1362 * Use only for debugging. 1363 * @param {Number} millis The amount of milliseconds. 1364 */ 1365 var wait = function(millis) { 1366 millis || (millis = Date.ONESECOND); 1367 var now = new Date; 1368 while (new Date - now < millis) { 1369 void null; 1370 } 1371 return; 1372 } 1373