1 //
  2 // The Antville Project
  3 // http://code.google.com/p/antville
  4 //
  5 // Copyright 2001-2007 by The Antville People
  6 //
  7 // Licensed under the Apache License, Version 2.0 (the ``License'');
  8 // you may not use this file except in compliance with the License.
  9 // You may obtain a copy of the License at
 10 //
 11 //    http://www.apache.org/licenses/LICENSE-2.0
 12 //
 13 // Unless required by applicable law or agreed to in writing, software
 14 // distributed under the License is distributed on an ``AS IS'' BASIS,
 15 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 16 // See the License for the specific language governing permissions and
 17 // limitations under the License.
 18 //
 19 // $Revision$
 20 // $LastChangedBy$
 21 // $LastChangedDate$
 22 // $URL$
 23 //
 24 
 25 /**
 26  * @fileOverview Defines global variables and functions.
 27  */
 28 
 29 app.addRepository(app.dir + "/../lib/rome-1.0.jar");
 30 app.addRepository(app.dir + "/../lib/jdom.jar");
 31 app.addRepository(app.dir + "/../lib/itunes-0.4.jar");
 32 
 33 app.addRepository("modules/core/Global.js");
 34 app.addRepository("modules/core/HopObject.js");
 35 app.addRepository("modules/core/Number.js");
 36 app.addRepository("modules/core/Filters.js");
 37 
 38 app.addRepository("modules/helma/Image.js");
 39 app.addRepository("modules/helma/Html.js");
 40 app.addRepository("modules/helma/Http.js");
 41 app.addRepository("modules/helma/Mail.js");
 42 app.addRepository("modules/helma/Zip.js");
 43 
 44 app.addRepository("modules/jala/code/Date.js");
 45 app.addRepository("modules/jala/code/HopObject.js");
 46 app.addRepository("modules/jala/code/ListRenderer.js");
 47 app.addRepository("modules/jala/code/Utilities.js");
 48 
 49 app.addRepository(app.dir + "/../i18n/messages.en.js");
 50 app.addRepository(app.dir + "/../i18n/messages.de.js");
 51 app.addRepository("modules/jala/code/I18n.js"); // Pls. test before moving around!
 52 
 53 // FIXME: Overwriting some String patterns and methods with more reliable ones
 54 // Regex contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/
 55 // licensed unter MIT license - http://www.opensource.org/licenses/mit-license.php
 56 /**
 57  * @name String
 58  * @namespace
 59  */
 60 /** @constant */
 61 String.EMAILPATTERN = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i;
 62 /** @constant */
 63 String.URLPATTERN = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
 64 
 65 // FIXME: Be careful with property names of app.data;
 66 // they inherit all properties from HopObject!
 67 
 68 /**
 69  * @name app.data 
 70  * @namespace
 71  */
 72 /** @name app.data.entries */
 73 app.data.entries || (app.data.entries = []);
 74 /** @name app.data.mails */
 75 app.data.mails || (app.data.mails = []);
 76 /** @name app.data.requests */
 77 app.data.requests || (app.data.requests = {});
 78 /** @name app.data.callbacks */
 79 app.data.callbacks || (app.data.callbacks = []);
 80 /** @name app.data.exports */
 81 app.data.exports || (app.data.exports = []);
 82 /** @name app.data.imports */
 83 app.data.imports || (app.data.imports = []);
 84 
 85 /**
 86  * @name helma.Mail
 87  * @namespace
 88  */
 89 /**
 90  * Extend the Mail prototype with a method that simply adds a mail object 
 91  * to an application-wide array (mail queue).
 92  * @returns {Number} The number of mails waiting in the queue
 93  */
 94 helma.Mail.prototype.queue = function() {
 95    return app.data.mails.push(this);
 96 }
 97 
 98 /**
 99  * 
100  */
101 helma.Mail.flushQueue = function() {
102    if (app.data.mails.length > 0) {
103       app.debug("Flushing mail queue, sending " + 
104             app.data.mails.length + " messages");
105       var mail;
106       while (app.data.mails.length > 0) {
107          mail = app.data.mails.pop();
108          mail.send();
109          if (mail.status > 0) {
110             app.debug("Error while sending e-mail (status " + mail.status + ")");
111             mail.writeToFile(getProperty("smtp.dir"));
112          }
113       }
114    }
115    return;
116 }
117 
118 jala.i18n.setLocaleGetter(function() {
119    return res.handlers.site.getLocale();
120 });
121 
122 /** @constant */
123 var SHORTDATEFORMAT = "yyyy-MM-dd HH:mm";
124 /** @constant */
125 var LONGDATEFORMAT = "EEEE, d. MMMM yyyy, HH:mm";
126 /** @constant */
127 var SQLDATEFORMAT = "yyyy-MM-dd HH:mm:ss";
128 
129 /** */
130 var idle = new Function;
131 /** */
132 var html = new helma.Html();
133 
134 /** */
135 var rome = new JavaImporter(
136    Packages.com.sun.syndication.feed.synd,
137    Packages.com.sun.syndication.io.SyndFeedOutput,
138    Packages.com.sun.syndication.feed.module.itunes,
139    Packages.com.sun.syndication.feed.module.itunes.types
140 );
141 
142 /**
143  * 
144  */
145 function onStart() {
146    if (typeof root === "undefined") {
147       app.logger.error("Error in database configuration: no root site found.");
148       return;
149    }
150    app.addCronJob("nightly", "*", "*", "*", "*", "5", "0");      
151    // FIXME: Does database exist?
152    /*var db = getDBConnection("antville");
153    var rows = db.executeRetrieval("select min(id) as id from site");
154    rows.next();
155    var id = rows.getColumnItem("id");
156    //Packages.helma.main.Server.getServer().stopApplication(app.name);
157    rows.release();*/
158    return;
159 }
160 
161 /**
162  * 
163  * @param {HopObject} ctor
164  * @returns {Function} 
165  */
166 function defineConstants(ctor /*, arguments */) {
167    var constants = [], name;
168    for (var i=1; i<arguments.length; i+=1) {
169       name = arguments[i].toUpperCase().replace(/\s/g, "");
170       if (ctor[name]) {
171          //app.logger.warn("Constant already defined: " + ctor.name + "." + name);
172       }
173       ctor[name] = arguments[i];
174       constants.push({
175          value: arguments[i],
176          display: arguments[i]
177       });
178    }
179    return function() {
180       return constants;
181    };
182 }
183 
184 /**
185  * Disable a macro with the idle function
186  * @param {HopObject} ctor
187  * @param {String} name
188  * @returns {Function}
189  */
190 function disableMacro(ctor, name) {
191    return ctor.prototype[name + "_macro"] = idle;
192 }
193 
194 /**
195  * @returns {Number} The period in milliseconds the scheduler will be 
196  * called again. 
197  */
198 function scheduler() {
199    Root.commitEntries();
200    Root.commitRequests();
201    Root.invokeCallbacks();
202    Root.updateHealth();
203    Root.exportImport();
204    helma.Mail.flushQueue();
205    return 5000;
206 }
207 
208 /**
209  * 
210  */
211 function nightly() {
212    Root.purgeReferrers();
213    //Admin.purgeDatabase();
214    return;
215 }
216 
217 /**
218  * Renders a string depending on the comparison of two values. If the first 
219  * value equals the second value, the first result will be returned; the 
220  * second result otherwise.
221  * <p>Example: <code><% if <% macro %> is "value" then "yes!" else "no :(" %></code>
222  * </p>
223  * Note that any value or result can be a macro, too. Thus, this can be used as
224  * a simple implementation of an if-then-else statement by using Helma macros
225  * only. 
226  * @param {Object} param The default Helma macro parameter object
227  * @param {String} firstValue The first value
228  * @param {String} _is_ Syntactic sugar; should be "is" for legibility
229  * @param {String} secondValue The second value
230  * @param {String} _then_ Syntactic sugar; should be "then" for legibility
231  * @param {String} firstResult The first result, ie. the value that will be 
232  * returned if the first value equals the second one
233  * @param {String} _else_ Syntactic sugar; should be "else" for legibility
234  * @param {String} secondResult The second result, ie. the value that will be 
235  * returned if the first value does not equal the second one
236  * @returns {String} The resulting value
237  */
238 function if_macro(param, firstValue, _is_, secondValue, _then_, firstResult, 
239       _else_, secondResult) {
240    return (("" + firstValue) == ("" + secondValue)) ? firstResult : secondResult;
241 }
242 
243 /**
244  * 
245  * @param {Object} param
246  * @param {String} text
247  */
248 function gettext_macro(param, text /*, value1, value2, ...*/) {
249    var args = [text];
250    for (var i=2; i<arguments.length; i+=1) {
251       args.push(arguments[i]);
252    }
253    return gettext.apply(this, args);
254 }
255 
256 /**
257  * 
258  * @param {Object} param
259  * @param {String} singular
260  * @param {String} plural
261  */
262 function ngettext_macro(param, singular, plural /*, value1, value2, ...*/) {
263    var args = [singular, plural];
264    for (var i=3; i<arguments.length; i+=1) {
265       args.push(arguments[i]);
266    }
267    return ngettext.apply(this, args);
268 }
269 
270 /**
271  * 
272  * @param {Object} param
273  * @param {String} format
274  * @returns {String} The formatted current date string
275  * @see formatDate
276  */
277 function now_macro(param, format) {
278    return formatDate(new Date, format || param.format);
279 }
280 
281 /**
282  * @returns {String} The rendered link element
283  * @see renderLink
284  */
285 function link_macro() {
286    return renderLink.apply(this, arguments);
287 }
288 
289 // FIXME: The definition with "var" is necessary; otherwise the skin_macro()
290 // method won't be overwritten reliably. (Looks like a Helma bug.)
291 /**
292  * 
293  * @param {Object} param
294  * @param {String} name
295  * @returns {String} The rendered skin
296  * @see HopObject#skin_macro
297  */
298 var skin_macro = function(param, name) {
299   return HopObject.prototype.skin_macro.apply(this, arguments);
300 }
301 
302 /**
303  * 
304  * @param {Object} param
305  * @param {String} delimiter
306  */
307 function breadcrumbs_macro (param, delimiter) {
308    delimiter || (delimiter = param.separator || " : ");
309    var offset = res.handlers.site === root ? 0 : 1;
310    for (var i=offset; i<path.length-1; i+=1) {
311       html.link({href: path[i].href()}, path[i].getTitle());
312       res.write(delimiter);
313    }
314    var title = path[i].getTitle();
315    if (req.action !== "main" && path[i].main_action) {
316       html.link({href: path[i].href()}, title);
317    } else {
318       res.write(title);
319    }
320    return;
321 }
322 
323 /**
324  * 
325  * @param {Object} param
326  * @param {String} id
327  * @param {String} mode
328  */
329 function story_macro(param, id, mode) {
330    var story = HopObject.getFromPath(id, "stories");
331    if (!story || !story.getPermission("main")) {
332       return;
333    }
334 
335    switch (mode) {
336       case "url":
337       res.write(story.href());
338       break;
339       case "link":
340       html.link({href: story.href()}, story.getTitle());
341       break;
342       default:
343       story.renderSkin("Story#" + (param.skin || "embed"));
344    }
345    return;
346 }
347 
348 /**
349  * 
350  * @param {Object} param
351  * @param {String} id
352  * @param {String} mode
353  */
354 function file_macro(param, id, mode) {
355    if (!id) {
356       return;
357    }
358 
359    var file;
360    if (id.startsWith("/")) {
361       name = id.substring(1);
362       if (mode === "url") {
363          res.write(root.getStaticUrl(name));
364       } else {
365          file = root.getStaticFile(name);
366          res.push();
367          File.prototype.contentLength_macro.call({
368             contentLength: file.getLength()
369          });
370          res.handlers.file = {
371             href: root.getStaticUrl(name),
372             name: name,
373             contentLength: res.pop()
374          };
375          File.prototype.renderSkin("File#main");
376       }
377       return;
378    }
379 
380    file = HopObject.getFromPath(id, "files");
381    if (!file) {
382       return;
383    }
384    if (mode === "url") {
385       res.write(file.getUrl());
386    } else {
387       file.renderSkin(param.skin || "File#main");
388    }
389    return;
390 }
391 
392 /**
393  * 
394  * @param {Object} param
395  * @param {String} id
396  * @param {String} mode
397  */
398 function image_macro(param, id, mode) {
399    if (!id) {
400       return;
401    }
402 
403    var image;
404    if (id.startsWith("/")) {
405       var name = id.substring(1);
406       image = Images.Default[name] || Images.Default[name + ".gif"];
407    } else {
408       image = HopObject.getFromPath(id, "images");
409       if (!image && param.fallback) {
410          image = HopObject.getFromPath(param.fallback, "images");
411       }
412    }
413 
414    if (!image) {
415       return;
416    }
417    
418    switch (mode) {
419       case "url":
420       res.write(image.getUrl());
421       break;
422       case "thumbnail":
423       case "popup":
424       var url = image.getUrl();
425       html.openTag("a", {href: url});
426       // FIXME: Bloody popups belong to compatibility layer
427       (mode === "popup") && (param.onclick = 'javascript:openPopup(\'' + 
428             url + '\', ' + image.width + ', ' + image.height + '); return false;')
429       image.thumbnail_macro(param);
430       html.closeTag("a");
431       break;
432       default:
433       image.render_macro(param);
434    }
435    return;
436 }
437 
438 /**
439  * 
440  * @param {Object} param
441  * @param {String} id
442  * @param {String} mode
443  */
444 function poll_macro(param, id, mode) {
445    if (!id) {
446       return;
447    }
448 
449    var poll = HopObject.getFromPath(id, "polls");
450    if (!poll) {
451       return;
452    }
453 
454    switch (mode) {
455       case "url":
456       res.write(poll.href());
457       break;
458       case "link":
459       html.link({
460          href: poll.href(poll.status === Poll.CLOSED ? "result" : "")
461       }, poll.question);
462       break;
463       default:
464       if (poll.status === Poll.CLOSED || mode === "results")
465          poll.renderSkin("$Poll#results");
466       else {
467          poll.renderSkin("$Poll#main");
468       }
469    }
470    return;
471 }
472 
473 /**
474  * 
475  * @param {Object} param
476  * @param {String} id
477  * @param {String} limit
478  */
479 function list_macro(param, id, limit) {
480    if (!id) {
481       return;
482    }
483    
484    var max = Math.min(limit || 25, 50);
485    var collection, skin;
486    if (id === "sites") {
487       collection = root.sites.list(0, max);
488       skin = "Site#preview";
489    } else if (id === "updates") {
490       collection = root.updates.list(0, limit);
491       skin = "Site#preview";
492    } else {
493       var site;
494       var parts = id.split("/");
495       if (parts.length > 1) {
496          type = parts[1];
497          site = root.sites.get(parts[0]);
498       } else {
499          type = parts[0];
500       }
501 
502       site || (site = res.handlers.site);
503       var filter = function(item, index) {
504          return index < max && item.getPermission("main");
505       }
506       
507       var commentFilter = function(item) {
508          if (item.story.status !== Story.CLOSED && 
509                item.site.commentMode !== Site.DISABLED &&
510                item.story.commentMode !== Story.CLOSED) {
511             return true;
512          }
513          return false;
514       }
515 
516       switch (type) {
517          case "comments":
518          var comments = site.stories.comments;
519          collection = comments.list().filter(filter).filter(commentFilter);
520          skin = "Story#preview";
521          break;
522          
523          case "featured":
524          collection = site.stories.featured.list(0, max);
525          skin = "Story#preview";
526          break;
527          
528          case "images":
529          collection = site.images.list(0, max);
530          skin = "Image#preview";
531          break;
532          
533          case "postings":
534          content = site.stories.union;
535          collection = content.list().filter(filter).filter(function(item) {
536             if (item.constructor === Comment) {
537                return commentFilter(item);
538             }
539             return true;
540          });
541          skin = "Story#preview";
542          break;
543          
544          case "stories":
545          var stories = site.stories.recent;
546          var counter = 0;
547          collection = stories.list().filter(function(item, index) {
548             return item.constructor === Story && filter(item, counter++);
549          });
550          skin = "Story#preview";
551          break;
552          
553          case "tags":
554          return site.tags.list_macro(param, param.skin || "$Tag#preview");
555          break;
556          
557          default:
558          break;
559       }
560    }
561    param.skin && (skin = param.skin);
562    for each (var item in collection) {
563       // FIXME: Work-around for "story" handlers in comment skins
564       // (Obsolete as soon as "story" handlers are replaced with "this")
565       if (item.constructor === Comment) {
566          res.handlers.story = item;
567       }
568       item.renderSkin(skin);
569    }
570    return;
571 }
572 
573 /**
574  * 
575  * @param {Object} param
576  * @param {String} name
577  * @param {String} value
578  */
579 function value_macro(param, name, value) {
580    if (!name) {
581       return;
582    }
583    name = name.toLowerCase();
584    if (!value) {
585       res.write(res.meta.values[name]);
586    } else {
587       //res.write("/* set " + name + " to " + value + " */");
588       res.meta.values[name] = value;
589    }
590    return;
591 }
592 
593 /**
594  * 
595  * @param {Object} param
596  * @param {String} id
597  */
598 function randomize_macro(param, id) {
599    var getRandom = function(n) {
600       return Math.floor(Math.random() * n);
601    };
602 
603    var site;
604    if (id === "sites") {
605       site = root.sites.get(getRandom(root.sites.size()));
606       site.renderSkin(param.skin || "Site#preview");
607       return;
608    }
609 
610    var parts = id.split("/");
611    if (parts.length > 1) {
612       type = parts[1];
613       site = root.sites.get(parts[0]);
614    } else {
615       type = parts[0];
616    }
617    site || (site = res.handlers.site);
618    switch (type) {
619       case "stories":
620       var stories = site.stories["public"];
621       var story = stories.get(getRandom(stories.size()));
622       story && story.renderSkin(param.skin || "Story#preview");
623       break;
624       case "images":
625       var image = site.images.get(getRandom(site.images.size()));
626       image && image.renderSkin(param.skin || "Image#preview");
627       break;
628    }
629    return;
630 }
631 
632 /**
633  * 
634  * @param {Object} param
635  * @param {String} url
636  * @param {String} text
637  * @param {HopObject} handler
638  */
639 function renderLink(param, url, text, handler) {
640    url || (url = param.url || "");
641    text || (text = param.text || url);
642    if (!text || (handler && !handler.href)) {
643       return;
644    }
645    delete param.url;
646    delete param.text;
647    param.title || (param.title = String.EMPTY);
648    if (!handler || url.contains(":")) {
649       param.href = url;
650    } else if (url.contains("/") || url.contains("?") || url.contains("#")) {
651       var parts = url.split(/(\/|\?|#)/);
652       param.href = handler.href(parts[0]) + parts.splice(1).join(String.EMPTY);
653    } else {
654       param.href = handler.href(url);
655    }
656    html.link(param, text);
657    return;
658 }
659 
660 /**
661  * 
662  * @param {String} str
663  * @returns {String|null} The e-mail string if valid, null otherwise
664  */
665 function validateEmail(str) {
666 	if (str) {
667       if (str.isEmail(str)) {
668          return str;
669       }
670    }
671    return null;
672 }
673 
674 /**
675  * 
676  * @param {String} str
677  * @returns {String|null} The URL string if valid, null otherwise
678  */
679 function validateUrl(str) {
680    if (str) {
681       if (str.isUrl(str)) {
682          return String(str);
683       } else if (str.contains("@")) {
684          return "mailto:" + str;
685       } else {
686          return null;
687       }
688    }
689    return null;
690 }
691 
692 /**
693  * 
694  * @param {String} str
695  * @returns {String} The processed string
696  */
697 function quote(str) {
698    if (/[\W\D]/.test(str)) {
699       str = '"' + str + '"';
700    }
701    return str;
702 }
703 
704 /**
705  * 
706  * @param {Number} number
707  * @param {String} pattern
708  * @returns {String} The formatted number string
709  */
710 function formatNumber(number, pattern) {
711    return Number(number).format(pattern, res.handlers.site.getLocale());
712 }
713 
714 /**
715  * 
716  * @param {Date} date
717  * @param {pattern} pattern
718  * @returns {String} The formatted date string
719  */
720 function formatDate(date, pattern) {
721    if (!date) {
722       return null;
723    }
724    pattern || (pattern = "short");
725    var site = res.handlers.site;
726    var format = site.metadata.get(pattern.toLowerCase() + "DateFormat");
727    if (!format) {
728       format = global[pattern.toUpperCase() + "DATEFORMAT"] || pattern;
729    }
730    try {
731       return date.format(format, site.getLocale(), site.getTimeZone());
732    } catch (ex) {
733       return "[Macro error: Invalid date format]";
734    }
735    return;
736 }
737 
738 /**
739  * Injects the XSLT stylesheet declaration into an XML string until  
740  * Mozilla developers will have mercy.
741  * @param {String} xml An XML string
742  * @returns {String} An XML string containing the XSLT stylesheet declaration
743  */
744 function injectXslDeclaration(xml) {
745    res.push();
746    renderSkin("Global#xslDeclaration");
747    return xml.replace(/(\?>\r?\n?)/, "$1" + res.pop());
748 }
749 
750 /**
751  * General mail sending function. Mails will be queued in app.data.mails.
752  * @param {String} sender The sender's e-mail address
753  * @param {Object} recipient The recipient's email addresses
754  * @param {String} subject The e-mail's subject
755  * @param {String} body The body text of the e-mail
756  * @returns {Number} The status code of the underlying helma.Mail instance
757  */
758 function sendMail(sender, recipient, subject, body) {
759    if (!recipient || !body) {
760       throw Error("Insufficient arguments in method sendMail()");
761    }
762    var mail = new helma.Mail(getProperty("smtp", "localhost"), getProperty("smtp.port", "25"));
763    mail.setFrom("noreply@antville.org");
764    if (recipient instanceof Array) {
765       for (var i in recipient) {
766          mail.addBCC(recipient[i]);
767       }
768    } else {
769       mail.addTo(recipient);
770    }
771    mail.setSubject(subject);
772    mail.setText(body);
773    mail.addText(renderSkinAsString("$Global#disclaimer"));
774    mail.queue();
775    return mail.status;
776 }
777 
778 /**
779  * 
780  * @param {String} language
781  * @returns {java.util.Locale} The corresponding locale object
782  */
783 function getLocale(language) {
784    return java.util.Locale[(language || "english").toUpperCase()];
785 }
786 
787 /**
788  * Creates an array of all available Java locales sorted by their names.
789  * @param {String} language The optional language of the locales
790  * @returns {Object[]} A sorted array containing the corresponding locales
791  */
792 function getLocales(language) {
793    var result = [], locale;
794    var displayLocale = getLocale(language);
795    var locales = java.util.Locale.getAvailableLocales();
796    for (var i in locales) {
797       locale = locales[i].toString();
798       result.push({
799          value: locale,
800          display: locales[i].getDisplayName(displayLocale),
801       });
802    }
803    result.sort(new String.Sorter("display"));
804    return result;
805 }
806 
807 /**
808  * 
809  * @param {String} language
810  * @returns {Object[]} A sorted array containing the corresponding timezones
811  */
812 function getTimeZones(language) {
813    var result = [], timeZone, offset;
814    var locale = getLocale(language); 
815    var zones = java.util.TimeZone.getAvailableIDs();
816    for each (var zone in zones) {
817       timeZone = java.util.TimeZone.getTimeZone(zone);
818       offset = timeZone.getRawOffset();
819       /*res.debug("zone id: " +  zone)
820       res.debug("offset: " + timeZone.getRawOffset())
821       res.debug("dst savings: " + timeZone.getDSTSavings())
822       res.debug("old code: " + formatter.format(offset / Date.ONEHOUR))
823       res.debug("new code: " + (offset / Date.ONEHOUR).format("+00;-00") + ":" + 
824             (Math.abs(offset % Date.ONEHOUR) / Date.ONEMINUTE).format("00"))*/
825       result.push({
826          value: zone,
827          display: zone + " (UTC" + (offset / Date.ONEHOUR).format("+00;-00") + 
828                ":" + (Math.abs(offset % Date.ONEHOUR) / 
829                Date.ONEMINUTE).format("00") + ")"
830       });
831    }
832    result.sort(new String.Sorter("display"));
833    return result;   
834 }
835 
836 /**
837  * 
838  * @param {String} type
839  * @param {String} language
840  * @returns {Array[]} An array containing the corresponding date formats
841  */
842 function getDateFormats(type, language) {
843    var patterns;
844    if (type === "short") {
845       patterns = [SHORTDATEFORMAT, "yyyy/MM/dd HH:mm", 
846             "yyyy.MM.dd, HH:mm", "d. MMMM, HH:mm", "MMMM d, HH:mm", 
847             "d. MMM, HH:mm", "MMM d, HH:mm", "EEE, d. MMM, HH:mm", 
848             "EEE MMM d, HH:mm", "EEE, HH:mm", "EE, HH:mm", "HH:mm"];
849    } else if (type === "long") {
850       patterns = [LONGDATEFORMAT, "EEEE, MMMM dd, yyyy, HH:mm", 
851             "EE, d. MMM. yyyy, HH:mm", "EE MMM dd, yyyy, HH:mm", 
852             "EE yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm", "d. MMMM yyyy, HH:mm", 
853             "MMMM d, yyyy, HH:mm", "d. MMM yyyy, HH:mm", "MMM d, yyyy, HH:mm"];
854    }
855    var result = [], sdf;
856    var locale = getLocale(language);
857    var now = new Date;
858    for each (var pattern in patterns) {
859       sdf = new java.text.SimpleDateFormat(pattern, locale);
860       result.push([encodeForm(pattern), sdf.format(now)]);
861    }
862    return result;
863 }
864 
865 /**
866  * 
867  * @param {Object} value
868  * @param {Object} param
869  * @param {Object} defaultValue
870  * @returns {Object} The value argument if truthy, the defaultValue argument
871  * otherwise
872  */
873 function default_filter(value, param, defaultValue) {
874    return value || defaultValue;
875 }
876 
877 /**
878  * 
879  * @param {Date} value
880  * @param {Object} param
881  * @returns {String} The age string of a date
882  */
883 function age_filter(value, param) {
884    if (!value || value.constructor !== Date) {
885       return value;
886    }
887    return value.getAge()
888 }
889 
890 /**
891  * 
892  * @param {String} text
893  * @param {String} param
894  * @param {Object} url
895  * @returns {String} The rendered link element
896  * @see renderLink
897  */
898 function link_filter(text, param, url) {
899    if (text) { 
900       url || (url = text);
901       return renderLink(param, url, text);
902    }
903    return;
904 }
905 
906 /**
907  * 
908  * @param {Object} string
909  * @param {Object} param
910  * @param {String} pattern
911  * @returns {String} The formatted string
912  */
913 function format_filter(value, param, pattern) {
914    if (!value && value !== 0) {
915       return;
916    }
917    var f = global["format" + value.constructor.name];
918    if (f && f.constructor === Function) {
919       return f(value, pattern || param.pattern);
920    }
921    return value;
922 }
923 
924 /**
925  * 
926  * @param {String} input
927  * @param {Object} param
928  * @param {Number} limit
929  * @param {String} clipping
930  * @param {String} delimiter
931  * @returns {String} The clipped input
932  */
933 function clip_filter(input, param, limit, clipping, delimiter) {
934    var len = 0;
935    if (input) {
936       len = input.length;
937       input = input.stripTags();
938    }
939    input || (input = ngettext("({0} character)", "({0} characters)", len));
940    limit || (limit = 20);
941    clipping || (clipping = "...");
942    delimiter || (delimiter = "\\s");
943    return String(input || "").head(limit, clipping, delimiter);
944 }
945 
946 // FIXME:
947 /**
948  * 
949  * @param {String} rss
950  * @returns {String} The fixed RSS string
951  */
952 function fixRssText(rss) {
953    var re = new RegExp("<img src\\s*=\\s*\"?([^\\s\"]+)?\"?[^>]*?(alt\\s*=\\s*\"?([^\"]+)?\"?[^>]*?)?>", "gi");
954    rss = rss.replace(re, "[<a href=\"$1\" title=\"$3\">Image</a>]");
955    return rss;
956 }
957 
958 // FIXME:
959 /**
960  * 
961  */
962 function countUsers() {
963    app.data.activeUsers = new Array();
964    var l = app.getActiveUsers();
965    for (var i in l)
966       app.data.activeUsers[app.data.activeUsers.length] = l[i];
967    l = app.getSessions();
968    app.data.sessions = 0;
969    for (var i in l) {
970       if (!l[i].user)
971          app.data.sessions++;
972    }
973    app.data.activeUsers.sort();
974    return;
975 }
976 
977 // FIXME:
978 /**
979  * @ignore
980  * @param {Object} src
981  */
982 function doWikiStuff (src) {
983    // robert, disabled: didn't get the reason for this:
984    // var src= " "+src;
985    if (src == null || !src.contains("<*"))
986       return src;
987 
988    // do the Wiki link thing, <*asterisk style*>
989    var regex = new RegExp ("<[*]([^*]+)[*]>");
990    regex.ignoreCase=true;
991    
992    var text = "";
993    var start = 0;
994    while (true) {
995       var found = regex.exec (src.substring(start));
996       var to = found == null ? src.length : start + found.index;
997       text += src.substring(start, to);
998       if (found == null)
999          break;
1000       var name = ""+(new java.lang.String (found[1])).trim();
1001       var item = res.handlers.site.topics.get (name);
1002       if (item == null && name.lastIndexOf("s") == name.length-1)
1003          item = res.handlers.site.topics.get (name.substring(0, name.length-1));
1004       if (item == null || !item.size())
1005          text += format(name)+" <small>[<a href=\""+res.handlers.site.stories.href("create")+"?topic="+escape(name)+"\">define "+format(name)+"</a>]</small>";
1006       else
1007          text += "<a href=\""+item.href()+"\">"+name+"</a>";
1008       start += found.index + found[1].length+4;
1009    }
1010    return text;
1011 }
1012 
1013 // FIXME: Rewrite with jala.ListRenderer?
1014 /**
1015  * 
1016  * @param {HopObject|Array} collection
1017  * @param {Function|Skin} funcOrSkin
1018  * @param {Number} itemsPerPage
1019  * @param {Number} pageIdx
1020  * @returns {String} The rendered list
1021  */
1022 function renderList(collection, funcOrSkin, itemsPerPage, pageIdx) {
1023    var currIdx = 0, item;
1024    var isArray = collection instanceof Array;
1025    var stop = size = isArray ? collection.length : collection.size();
1026 
1027    if (itemsPerPage) {
1028       var totalPages = Math.ceil(size/itemsPerPage);
1029       if (isNaN(pageIdx) || pageIdx > totalPages || pageIdx < 0) {
1030          pageIdx = 0;
1031       }
1032       currIdx = pageIdx * itemsPerPage;
1033       stop = Math.min(currIdx + itemsPerPage, size);
1034    }
1035 
1036    var isFunction = (funcOrSkin instanceof Function) ? true : false;
1037    res.push();
1038    while (currIdx < stop) {
1039       item = isArray ? collection[currIdx] : collection.get(currIdx);
1040       isFunction ? funcOrSkin(item) : item.renderSkin(funcOrSkin);
1041       currIdx += 1;
1042    }
1043    return res.pop();
1044 }
1045 
1046 // FIXME: Rewrite using jala.ListRenderer or rename (eg. renderIndex)
1047 /**
1048  * 
1049  * @param {HopObject|Array|Number} collectionOrSize
1050  * @param {String} url
1051  * @param {Number} itemsPerPage
1052  * @param {Number} pageIdx
1053  * @returns {String} The rendered index
1054  */
1055 function renderPager(collectionOrSize, url, itemsPerPage, pageIdx) {
1056    // Render a single item for the navigation bar
1057    var renderItem = function(text, cssClass, url, page) {
1058       var param = {"class": cssClass};
1059       if (!url) {
1060          param.text = text;
1061       } else {
1062          if (url.contains("?"))
1063             param.text = html.linkAsString({href: url + "&page=" + page}, text);
1064          else
1065             param.text = html.linkAsString({href: url + "?page=" + page}, text);
1066       }
1067       renderSkin("$Global#pagerItem", param);
1068       return;
1069    }
1070 
1071    var maxItems = 10;
1072    var size = 0;
1073    if (collectionOrSize instanceof Array) {
1074       size = collectionOrSize.length;
1075    } else if (collectionOrSize instanceof HopObject) {
1076       size = collectionOrSize.size();
1077    } else if (!isNaN(collectionOrSize)) {
1078       size = parseInt(collectionOrSize, 10);
1079    }
1080    var lastPageIdx = Math.ceil(size/itemsPerPage)-1;
1081    // If there's just one page no navigation will be rendered
1082    if (lastPageIdx <= 0) {
1083       return null;
1084    }
1085 
1086    // Initialize the parameter object
1087    var param = {};
1088    var pageIdx = parseInt(pageIdx, 10);
1089    // Check if the passed index is correct
1090    if (isNaN(pageIdx) || pageIdx > lastPageIdx || pageIdx < 0) {
1091       pageIdx = 0;
1092    }
1093    param.display = ((pageIdx * itemsPerPage) + 1) + "-" + 
1094          (Math.min((pageIdx * itemsPerPage) + itemsPerPage, size));
1095    param.total = size;
1096 
1097    // Render the navigation-bar
1098    res.push();
1099    (pageIdx > 0) && renderItem("[–]", "pageNavItem", url, pageIdx-1);
1100    var offset = Math.floor(pageIdx / maxItems) * maxItems;
1101    (offset > 0) && renderItem("[..]", "pageNavItem", url, offset-1);
1102    var currPage = offset;
1103    var stop = Math.min(currPage + maxItems, lastPageIdx+1);
1104    while (currPage < stop) {
1105       if (currPage === pageIdx) {
1106          renderItem("[" + (currPage +1) + "]", "pageNavSelItem");
1107       } else {
1108          renderItem("[" + (currPage +1) + "]", "pageNavItem", url, currPage);
1109       }
1110       currPage += 1;
1111    }
1112    if (currPage < lastPageIdx) {
1113       renderItem("[..]", "pageNavItem", url, offset + maxItems);
1114    }
1115    if (pageIdx < lastPageIdx) {
1116       renderItem("[+]", "pageNavItem", url, pageIdx +1);
1117    }
1118    param.pager = res.pop();
1119    return renderSkinAsString("$Global#pager", param);
1120 }
1121 
1122 /**
1123  * 
1124  * @param {String} plural
1125  * @returns {String} The english singular form of the input
1126  */
1127 function singularize(plural) {
1128    if (plural.endsWith("ies")) {
1129       return plural.substring(0, plural.length-3) + "y";
1130    }
1131    return plural.substring(0, plural.length-1);
1132 }
1133 
1134 /**
1135  * 
1136  * @param {String} singular
1137  * @returns {String} The english plural form of the input
1138  */
1139 function pluralize(singular) {
1140    if (singular.endsWith("y")) {
1141       return singular.substring(0, singular.length-1) + "ies";
1142    }
1143    return singular + "s";
1144 }
1145 
1146 /**
1147  * 
1148  * @param {Number} millis
1149  */
1150 var wait = function(millis) {
1151    millis || (millis = Date.ONESECOND);
1152    var now = new Date;
1153    while (new Date - now < millis) {
1154       void null;
1155    }
1156    return;
1157 }
1158