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