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