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