      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