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