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