* Added global app.data.extras collection and registerExtra() method * Defined res.handlers.extra in HopObject.onRequest() handler to provide extra macros * Implemented reCAPTCHA for anonymously contacting an Antville member as proof of concept
1257 lines
33 KiB
JavaScript
1257 lines
33 KiB
JavaScript
// The Antville Project
|
||
// http://code.google.com/p/antville
|
||
//
|
||
// Copyright 2007-2011 by Tobi Schäfer.
|
||
//
|
||
// Copyright 2001–2007 Robert Gaggl, Hannes Wallnöfer, Tobi Schäfer,
|
||
// Matthias & Michael Platzer, Christoph Lincke.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the ``License'');
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an ``AS IS'' BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
//
|
||
// $Revision$
|
||
// $Author$
|
||
// $Date$
|
||
// $URL$
|
||
|
||
/**
|
||
* @fileOverview Defines global variables and functions.
|
||
*/
|
||
|
||
app.addRepository(app.dir + "/../lib/rome-1.0.jar");
|
||
app.addRepository(app.dir + "/../lib/jdom.jar");
|
||
app.addRepository(app.dir + "/../lib/itunes-0.4.jar");
|
||
|
||
app.addRepository("modules/core/Global.js");
|
||
app.addRepository("modules/core/HopObject.js");
|
||
app.addRepository("modules/core/Filters.js");
|
||
app.addRepository("modules/core/JSON");
|
||
app.addRepository("modules/core/Number.js");
|
||
|
||
app.addRepository("modules/helma/File");
|
||
app.addRepository("modules/helma/Image.js");
|
||
app.addRepository("modules/helma/Html.js");
|
||
app.addRepository("modules/helma/Http.js");
|
||
app.addRepository("modules/helma/Mail.js");
|
||
app.addRepository("modules/helma/Zip.js");
|
||
|
||
app.addRepository("modules/jala/code/Date.js");
|
||
app.addRepository("modules/jala/code/HopObject.js");
|
||
app.addRepository("modules/jala/code/ListRenderer.js");
|
||
app.addRepository("modules/jala/code/Utilities.js");
|
||
|
||
var dir = new helma.File(app.dir, "../i18n");
|
||
for each (let fname in dir.list()) {
|
||
fname.endsWith(".js") && app.addRepository(app.dir + "/../i18n/" + fname);
|
||
}
|
||
// I18n.js needs to be added *after* the message files or the translations get lost
|
||
app.addRepository("modules/jala/code/I18n.js");
|
||
|
||
// FIXME: Be careful with property names of app.data;
|
||
// they inherit all properties from HopObject!
|
||
/**
|
||
* @name app.data
|
||
* @namespace
|
||
*/
|
||
/** @name app.data.callbacks */
|
||
app.data.callbacks || (app.data.callbacks = []);
|
||
/** @name app.data.entries */
|
||
app.data.entries || (app.data.entries = []);
|
||
/** @name app.data.features */
|
||
app.data.features || (app.data.features = []);
|
||
/** @name app.data.mails */
|
||
app.data.mails || (app.data.mails = []);
|
||
/** @name app.data.requests */
|
||
app.data.requests || (app.data.requests = {});
|
||
/** @name app.data.extras */
|
||
app.data.extras || (app.data.extras = {});
|
||
|
||
/**
|
||
* @name helma.File
|
||
* @namespace
|
||
*/
|
||
/**
|
||
* @param {helma.File} target
|
||
*/
|
||
helma.File.prototype.copyDirectory = function(target) {
|
||
/*
|
||
// Strange enough, Apache commons is not really faster...
|
||
var source = new java.io.File(this.toString());
|
||
target = new java.io.File(target.toString());
|
||
return Packages.org.apache.commons.io.FileUtils.copyDirectory(source, target);
|
||
*/
|
||
this.list().forEach(function(name) {
|
||
var file = new helma.File(this, name);
|
||
if (file.isDirectory()) {
|
||
file.copyDirectory(new helma.File(target, name));
|
||
} else {
|
||
target.makeDirectory();
|
||
file.hardCopy(new helma.File(target, name));
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* @name helma.Mail
|
||
* @namespace
|
||
*/
|
||
/**
|
||
* Extend the Mail prototype with a method that simply adds a mail object
|
||
* to an application-wide array (mail queue).
|
||
* @returns {Number} The number of mails waiting in the queue
|
||
*/
|
||
helma.Mail.prototype.queue = function() {
|
||
return app.data.mails.push(this);
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
helma.Mail.flushQueue = function() {
|
||
if (app.data.mails.length > 0) {
|
||
app.debug("Flushing mail queue, sending " +
|
||
app.data.mails.length + " messages");
|
||
var mail;
|
||
while (app.data.mails.length > 0) {
|
||
mail = app.data.mails.pop();
|
||
mail.send();
|
||
if (mail.status > 0) {
|
||
app.debug("Error while sending e-mail (status " + mail.status + ")");
|
||
mail.writeToFile(getProperty("smtp.dir"));
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
jala.i18n.setLocaleGetter(function() {
|
||
return (res.handlers.site || root).getLocale();
|
||
});
|
||
|
||
/** @constant */
|
||
var SQLDATEFORMAT = "yyyy-MM-dd HH:mm:ss";
|
||
|
||
// RegExp according to Jala’s HopObject.getAccessName()
|
||
/** @constant */
|
||
var NAMEPATTERN = /[\/+\\]/;
|
||
|
||
/** @function */
|
||
var idle = new Function;
|
||
|
||
/** */
|
||
var html = new helma.Html();
|
||
|
||
/** */
|
||
var rome = new JavaImporter(
|
||
Packages.com.sun.syndication.io,
|
||
Packages.com.sun.syndication.feed.synd,
|
||
Packages.com.sun.syndication.feed.module.itunes,
|
||
Packages.com.sun.syndication.feed.module.itunes.types
|
||
);
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function onStart() {
|
||
if (typeof root === "undefined") {
|
||
app.logger.error("Error in database configuration: no root site found.");
|
||
return;
|
||
}
|
||
// This is necessary once to be sure that aspect-oriented code will be applied
|
||
HopObject.prototype.onCodeUpdate && HopObject.prototype.onCodeUpdate();
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function onStop() { /* Currently empty, just to avoid annoying log message */ }
|
||
|
||
/**
|
||
*
|
||
* @param {HopObject} ctor
|
||
* @returns {Function}
|
||
*/
|
||
function defineConstants(ctor /*, arguments */) {
|
||
var constants = [], name;
|
||
for (var i=1; i<arguments.length; i+=1) {
|
||
name = arguments[i].toUpperCase().replace(/\s/g, "");
|
||
ctor[name] = arguments[i].toLowerCase();
|
||
constants.push(arguments[i]);
|
||
}
|
||
return function() {
|
||
return constants.map(function(item) {
|
||
return {
|
||
value: item.toLowerCase(),
|
||
display: gettext(item)
|
||
}
|
||
});
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Disable a macro with the idle function
|
||
* @param {HopObject} ctor
|
||
* @param {String} name
|
||
* @returns {Function}
|
||
*/
|
||
function disableMacro(ctor, name) {
|
||
return ctor.prototype[name + "_macro"] = idle;
|
||
}
|
||
|
||
/**
|
||
* @returns {Number} The period in milliseconds the scheduler will be
|
||
* called again.
|
||
*/
|
||
function scheduler() {
|
||
helma.Mail.flushQueue();
|
||
Admin.commitEntries();
|
||
Admin.commitRequests();
|
||
Admin.invokeCallbacks();
|
||
Admin.updateDomains();
|
||
Admin.updateHealth();
|
||
return app.properties.schedulerInterval;
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function nightly() {
|
||
var now = new Date;
|
||
if (now - (global.nightly.lastRun || -Infinity) < Date.ONEMINUTE) {
|
||
return; // Avoid running twice when main scheduler runs more than once per minute
|
||
}
|
||
app.log("***** Running nightly scripts *****");
|
||
Admin.purgeReferrers();
|
||
Admin.purgeSites();
|
||
Admin.dequeue();
|
||
global.nightly.lastRun = now;
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* Renders a string depending on the comparison of two values. If the first
|
||
* value equals the second value, the first result will be returned; the
|
||
* second result otherwise.
|
||
* <p>Example: <code><% if <% macro %> is "value" then "yes!" else "no :(" %></code>
|
||
* </p>
|
||
* Note that any value or result can be a macro, too. Thus, this can be used as
|
||
* a simple implementation of an if-then-else statement by using Helma macros
|
||
* only.
|
||
* @param {Object} param The default Helma macro parameter object
|
||
* @param {String} firstValue The first value
|
||
* @param {String} _is_ Syntactic sugar; should be "is" for legibility
|
||
* @param {String} secondValue The second value
|
||
* @param {String} _then_ Syntactic sugar; should be "then" for legibility
|
||
* @param {String} firstResult The first result, ie. the value that will be
|
||
* returned if the first value equals the second one
|
||
* @param {String} _else_ Syntactic sugar; should be "else" for legibility
|
||
* @param {String} secondResult The second result, ie. the value that will be
|
||
* returned if the first value does not equal the second one
|
||
* @returns {String} The resulting value
|
||
*/
|
||
function if_macro(param, firstValue, _is_, secondValue, _then_, firstResult,
|
||
_else_, secondResult) {
|
||
return (("" + firstValue) == ("" + secondValue)) ? firstResult : secondResult;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} format
|
||
* @returns {String} The formatted current date string
|
||
* @see formatDate
|
||
*/
|
||
function now_macro(param, format) {
|
||
return formatDate(new Date, format || param.format);
|
||
}
|
||
|
||
/**
|
||
* @returns {String} The rendered link element
|
||
* @see renderLink
|
||
*/
|
||
function link_macro() {
|
||
return renderLink.apply(this, arguments);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} name
|
||
* @returns {String} The rendered skin
|
||
* @see HopObject#skin_macro
|
||
*/
|
||
// FIXME: The definition with "var" is necessary; otherwise the skin_macro()
|
||
// method won't be overwritten reliably. (Looks like a Helma bug.)
|
||
var skin_macro = function(param, name) {
|
||
return HopObject.prototype.skin_macro.apply(this, arguments);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} delimiter
|
||
*/
|
||
function breadcrumbs_macro (param, delimiter) {
|
||
delimiter || (delimiter = param.separator || " : ");
|
||
//html.link({href: res.handlers.site.href()}, res.handlers.site.getTitle());
|
||
var offset = res.handlers.site === root ? 1 : 2;
|
||
for (var item, title, i=offset; i<path.length; i+=1) {
|
||
if (item = path[i]) {
|
||
if (!isNaN(item._id) && item.constructor !== Layout) {
|
||
continue;
|
||
}
|
||
if (i === path.length-1 && req.action === "main") {
|
||
res.write(item.getTitle());
|
||
} else {
|
||
html.link({href: path[i].href()}, item.getTitle());
|
||
}
|
||
(i < path.length-1) && res.write(delimiter);
|
||
}
|
||
}
|
||
if (req.action !== "main") {
|
||
res.write(delimiter);
|
||
res.write(gettext(req.action.titleize()));
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* Helper macro for checking if a user session is authenticated (logged in).
|
||
* Returns true if user is logged in, false otherwise.
|
||
* @returns Boolean
|
||
*/
|
||
function user_macro() {
|
||
return !!session.user;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
* @param {String} mode
|
||
*/
|
||
function story_macro(param, id, mode) {
|
||
var story = HopObject.getFromPath(id, "stories");
|
||
if (!story || !story.getPermission("main")) {
|
||
return;
|
||
}
|
||
|
||
switch (mode) {
|
||
case "url":
|
||
res.write(story.href());
|
||
break;
|
||
case "link":
|
||
html.link({href: story.href()}, story.getTitle());
|
||
break;
|
||
default:
|
||
story.renderSkin("Story#" + (param.skin || "embed"));
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
* @param {String} mode
|
||
*/
|
||
function file_macro(param, id, mode) {
|
||
if (!id) {
|
||
return;
|
||
}
|
||
|
||
var file;
|
||
if (id.startsWith("/")) {
|
||
name = id.substring(1);
|
||
if (mode === "url") {
|
||
res.write(root.getStaticUrl(name));
|
||
} else {
|
||
file = root.getStaticFile(name);
|
||
res.push();
|
||
File.prototype.contentLength_macro.call({
|
||
contentLength: file.getLength()
|
||
});
|
||
res.handlers.file = {
|
||
href: root.getStaticUrl(name),
|
||
name: name,
|
||
contentLength: res.pop()
|
||
};
|
||
File.prototype.renderSkin("File#main");
|
||
}
|
||
return;
|
||
}
|
||
|
||
file = HopObject.getFromPath(id, "files");
|
||
if (!file) {
|
||
return;
|
||
}
|
||
if (mode === "url") {
|
||
res.write(file.getUrl());
|
||
} else {
|
||
file.renderSkin(param.skin || "File#main");
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
* @param {String} mode
|
||
*/
|
||
function image_macro(param, id, mode) {
|
||
if (!id) {
|
||
return;
|
||
}
|
||
|
||
var image;
|
||
if (id.startsWith("/")) {
|
||
var name = id.substring(1);
|
||
image = Images.Default[name] || Images.Default[name + ".gif"];
|
||
} else {
|
||
image = HopObject.getFromPath(id, "images");
|
||
if (!image && param.fallback) {
|
||
image = HopObject.getFromPath(param.fallback, "images");
|
||
}
|
||
}
|
||
|
||
if (!image) {
|
||
return;
|
||
}
|
||
|
||
switch (mode) {
|
||
case "url":
|
||
res.write(image.getUrl());
|
||
break;
|
||
case "thumbnail":
|
||
case "popup":
|
||
var url = image.getUrl();
|
||
html.openTag("a", {href: url});
|
||
// FIXME: Bloody popups belong to compatibility layer
|
||
(mode === "popup") && (param.onclick = 'javascript:openPopup(\'' +
|
||
url + '\', ' + image.width + ', ' + image.height + '); return false;')
|
||
image.thumbnail_macro(param);
|
||
html.closeTag("a");
|
||
break;
|
||
default:
|
||
image.render_macro(param);
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
* @param {String} mode
|
||
*/
|
||
function poll_macro(param, id, mode) {
|
||
if (!id) {
|
||
return;
|
||
}
|
||
|
||
var poll = HopObject.getFromPath(id, "polls");
|
||
if (!poll) {
|
||
return;
|
||
}
|
||
|
||
switch (mode) {
|
||
case "url":
|
||
res.write(poll.href());
|
||
break;
|
||
case "link":
|
||
html.link({
|
||
href: poll.href(poll.status === Poll.CLOSED ? "result" : "")
|
||
}, poll.question);
|
||
break;
|
||
default:
|
||
if (poll.status === Poll.CLOSED || mode === "results")
|
||
poll.renderSkin("$Poll#results", {});
|
||
else {
|
||
poll.renderSkin("$Poll#main", {});
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
* @param {String} limit
|
||
*/
|
||
function list_macro(param, id, limit) {
|
||
if (!id) {
|
||
return;
|
||
}
|
||
|
||
var max = Math.min(limit || 25, 50);
|
||
var collection, skin;
|
||
if (id === "sites") {
|
||
collection = root.sites.list(0, max);
|
||
skin = "Site#preview";
|
||
} else if (id === "updates") {
|
||
collection = root.updates.list(0, limit);
|
||
skin = "Site#preview";
|
||
} else {
|
||
var site;
|
||
var parts = id.split("/");
|
||
if (parts.length > 1) {
|
||
type = parts[1];
|
||
site = root.sites.get(parts[0]);
|
||
} else {
|
||
type = parts[0];
|
||
}
|
||
|
||
site || (site = res.handlers.site);
|
||
var filter = function(item, index) {
|
||
return index < max && item.getPermission("main");
|
||
}
|
||
|
||
var commentFilter = function(item) {
|
||
if (item.story.status !== Story.CLOSED &&
|
||
item.site.commentMode !== Site.DISABLED &&
|
||
item.story.commentMode !== Story.CLOSED) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
switch (type) {
|
||
case "comments":
|
||
if (site.commentMode !== Site.DISABLED) {
|
||
var comments = site.stories.comments;
|
||
collection = comments.list().filter(filter);
|
||
skin = "Story#preview";
|
||
}
|
||
break;
|
||
|
||
case "featured":
|
||
collection = site.stories.featured.list(0, max);
|
||
skin = "Story#preview";
|
||
break;
|
||
|
||
case "images":
|
||
collection = site.images.list(0, max);
|
||
skin = "Image#preview";
|
||
break;
|
||
|
||
case "postings":
|
||
content = site.stories.union;
|
||
collection = content.list().filter(filter).filter(function(item) {
|
||
if (item.constructor === Comment) {
|
||
return commentFilter(item);
|
||
}
|
||
return true;
|
||
});
|
||
skin = "Story#preview";
|
||
break;
|
||
|
||
case "stories":
|
||
var stories = site.stories.recent;
|
||
var counter = 0;
|
||
collection = stories.list().filter(function(item, index) {
|
||
return item.constructor === Story && filter(item, counter++);
|
||
});
|
||
skin = "Story#preview";
|
||
break;
|
||
|
||
case "tags":
|
||
return site.tags.list_macro(param, param.skin || "$Tag#preview");
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
param.skin && (skin = param.skin);
|
||
for each (var item in collection) {
|
||
// FIXME: Work-around for "story" handlers in comment skins
|
||
// (Obsolete as soon as "story" handlers are replaced with "this")
|
||
if (item.constructor === Comment) {
|
||
res.handlers.story = item;
|
||
}
|
||
item.renderSkin(skin);
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} name
|
||
* @param {String} value
|
||
*/
|
||
function value_macro(param, name, value) {
|
||
if (!name) {
|
||
return;
|
||
}
|
||
name = name.toLowerCase();
|
||
if (!value) {
|
||
res.write(res.meta.values[name]);
|
||
} else {
|
||
//res.write("/* set " + name + " to " + value + " */");
|
||
res.meta.values[name] = value;
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} id
|
||
*/
|
||
function randomize_macro(param, id) {
|
||
var getRandom = function(n) {
|
||
return Math.floor(Math.random() * n);
|
||
};
|
||
|
||
var site;
|
||
if (id === "sites") {
|
||
site = root.sites.get(getRandom(root.sites.size()));
|
||
site.renderSkin(param.skin || "Site#preview");
|
||
return;
|
||
}
|
||
|
||
var parts = id.split("/");
|
||
if (parts.length > 1) {
|
||
type = parts[1];
|
||
site = root.sites.get(parts[0]);
|
||
} else {
|
||
type = parts[0];
|
||
}
|
||
site || (site = res.handlers.site);
|
||
switch (type) {
|
||
case "stories":
|
||
var stories = site.stories["public"];
|
||
var story = stories.get(getRandom(stories.size()));
|
||
story && story.renderSkin(param.skin || "Story#preview");
|
||
break;
|
||
case "images":
|
||
var image = site.images.get(getRandom(site.images.size()));
|
||
image && image.renderSkin(param.skin || "Image#preview");
|
||
break;
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function listItemFlag_macro(param, str) {
|
||
res.push();
|
||
for (var i=0; i<str.length; i+=1) {
|
||
res.write(str.charAt(i));
|
||
res.write("<br />");
|
||
}
|
||
renderSkin("$Global#listItemFlag", {text: res.pop()});
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} url
|
||
* @param {String} text
|
||
* @param {HopObject} handler
|
||
*/
|
||
function renderLink(param, url, text, handler) {
|
||
url || (url = param.url || String.EMPTY);
|
||
text || (text = param.text || url);
|
||
if (!text || (handler && !handler.href)) {
|
||
return;
|
||
}
|
||
if (url === "." || url === "main") {
|
||
url = String.EMPTY;
|
||
}
|
||
delete param.url;
|
||
delete param.text;
|
||
param.title || (param.title = String.EMPTY);
|
||
if (!handler || url.contains(":")) {
|
||
param.href = url;
|
||
} else if (url.contains("/") || url.contains("?") || url.contains("#")) {
|
||
var parts = url.split(/(\/|\?|#)/);
|
||
param.href = handler.href(parts[0]) + parts.splice(1).join(String.EMPTY);
|
||
} else {
|
||
param.href = handler.href(url);
|
||
}
|
||
html.link(param, text);
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} str
|
||
* @returns {String|null} The e-mail string if valid, null otherwise
|
||
*/
|
||
function validateEmail(str) {
|
||
if (str) {
|
||
if (str.isEmail()) {
|
||
return str;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} str
|
||
* @returns {String|null} The URL string if valid, null otherwise
|
||
*/
|
||
function validateUrl(str) {
|
||
if (str) {
|
||
if (str.isUrl()) {
|
||
return str;
|
||
} else if (str.isEmail()) {
|
||
return "mailto:" + str;
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} str
|
||
* @returns {String} The processed string
|
||
*/
|
||
function quote(str) {
|
||
if (/[\W\D]/.test(str)) {
|
||
str = '"' + str + '"';
|
||
}
|
||
return str;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Number} number
|
||
* @param {String} pattern
|
||
* @returns {String} The formatted number string
|
||
*/
|
||
function formatNumber(number, pattern) {
|
||
return Number(number).format(pattern, res.handlers.site.getLocale());
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Date} date
|
||
* @param {String} format
|
||
* @param {String} type
|
||
* @returns {String} The formatted date string
|
||
*/
|
||
function formatDate(date, format, type) {
|
||
if (!date) {
|
||
return null;
|
||
}
|
||
var pattern, site = res.handlers.site;
|
||
var locale = site.getLocale();
|
||
var type = java.text.DateFormat[type ? type.toUpperCase() : "FULL"];
|
||
switch (format) {
|
||
case null:
|
||
pattern = java.text.DateFormat.getDateTimeInstance(type, type, locale).toPattern();
|
||
break;
|
||
case "date":
|
||
pattern = java.text.DateFormat.getDateInstance(type, locale).toPattern();
|
||
break;
|
||
case "time":
|
||
pattern = java.text.DateFormat.getTimeInstance(type, locale).toPattern();
|
||
break;
|
||
case "short":
|
||
case "long":
|
||
pattern = site[format.toLowerCase() + "DateFormat"];
|
||
break;
|
||
default:
|
||
pattern = format;
|
||
}
|
||
try {
|
||
return date.format(pattern, site.getLocale(), site.getTimeZone());
|
||
} catch (ex) {
|
||
return "[Invalid date format]";
|
||
}
|
||
return String.EMPTY;
|
||
}
|
||
|
||
/**
|
||
* Injects the XSLT stylesheet declaration into an XML string until
|
||
* Mozilla developers will have mercy.
|
||
* @param {String} xml An XML string
|
||
* @returns {String} An XML string containing the XSLT stylesheet declaration
|
||
*/
|
||
function injectXslDeclaration(xml) {
|
||
res.push();
|
||
renderSkin("Global#xslDeclaration");
|
||
return xml.replace(/(\?>\r?\n?)/, "$1" + res.pop());
|
||
}
|
||
|
||
/**
|
||
* General mail sending function. Mails will be queued in app.data.mails.
|
||
* @param {Object} recipient The recipient's email addresses
|
||
* @param {String} subject The e-mail's subject
|
||
* @param {String} body The body text of the e-mail
|
||
* @returns {Number} The status code of the underlying helma.Mail instance
|
||
*/
|
||
function sendMail(recipient, subject, body, options) {
|
||
options || (options = {});
|
||
if (!recipient || !body) {
|
||
throw Error("Insufficient arguments in method sendMail()");
|
||
}
|
||
var mail = new helma.Mail(getProperty("smtp", "localhost"),
|
||
getProperty("smtp.port", "25"));
|
||
mail.setFrom(root.replyTo || "root@localhost");
|
||
if (recipient instanceof Array) {
|
||
for (var i in recipient) {
|
||
mail.addBCC(recipient[i]);
|
||
}
|
||
} else {
|
||
mail.addTo(recipient);
|
||
}
|
||
mail.setSubject(subject);
|
||
mail.setText(body);
|
||
if (options.footer !== false) { // It is the exception to have no footer
|
||
mail.addText(renderSkinAsString("$Global#mailFooter"));
|
||
}
|
||
mail.queue();
|
||
return mail.status;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} language
|
||
* @returns {java.util.Locale} The corresponding locale object
|
||
*/
|
||
function getLocale(language) {
|
||
return new java.util.Locale(language || "english");
|
||
}
|
||
|
||
/**
|
||
* Creates an array of all available Java locales sorted by their names.
|
||
* @param {String} language The optional language of the locales
|
||
* @returns {Object[]} A sorted array containing the corresponding locales
|
||
*/
|
||
function getLocales(language) {
|
||
var result = [], locale, localeString;
|
||
var locales = java.util.Locale.getAvailableLocales();
|
||
for (var i in locales) {
|
||
locale = locales[i];
|
||
localeString = locale.toString();
|
||
if (!localeString.contains("_")) {
|
||
result.push({
|
||
value: localeString,
|
||
display: locale.getDisplayName(locale),
|
||
"class": jala.i18n.getCatalog(jala.i18n.getLocale(localeString)) ? "translated" : ""
|
||
});
|
||
}
|
||
}
|
||
result.sort(new String.Sorter("display"));
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} language
|
||
* @returns {Object[]} A sorted array containing the corresponding timezones
|
||
*/
|
||
function getTimeZones(language) {
|
||
var result = [], timeZone, offset;
|
||
var locale = getLocale(language);
|
||
var zones = java.util.TimeZone.getAvailableIDs();
|
||
var now = new Date;
|
||
var previousZone;
|
||
for each (var zone in zones) {
|
||
timeZone = java.util.TimeZone.getTimeZone(zone);
|
||
if (!previousZone || !timeZone.hasSameRules(previousZone)) {
|
||
offset = timeZone.getRawOffset();
|
||
result.push({
|
||
value: zone,
|
||
display: timeZone.getDisplayName(timeZone.inDaylightTime(now),
|
||
java.util.TimeZone.LONG, locale) + " (UTC" + (offset /
|
||
Date.ONEHOUR).format("+00;-00") + ":" + (Math.abs(offset %
|
||
Date.ONEHOUR) / Date.ONEMINUTE).format("00") + ")"
|
||
});
|
||
}
|
||
previousZone = timeZone.clone();
|
||
}
|
||
result.sort(new String.Sorter("value"));
|
||
return result;
|
||
|
||
var group;
|
||
result.forEach(function(zone) {
|
||
var parts = zone.value.split("/");
|
||
if (parts.length > 1) {
|
||
if (parts[0] !== group) {
|
||
group = parts[0];
|
||
zone.group = group;
|
||
}
|
||
zone.display = parts.splice(1).join(String.EMPTY) + zone.display;
|
||
} else {
|
||
zone.display = zone.value + zone.display;
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} type
|
||
* @param {java.util.Locale} locale
|
||
* @returns {Array[]} An array containing the corresponding date formats
|
||
*/
|
||
function getDateFormats(type, locale, timeZone) {
|
||
type || (type = "short");
|
||
locale || (locale = java.util.Locale.getDefault());
|
||
timeZone || (timeZone = java.util.TimeZone.getDefault());
|
||
var types = [type];
|
||
types.push(type === "long" ? "full" : "medium");
|
||
var now = new Date, result = [], patterns = {};
|
||
for each (let dateType in types) {
|
||
let dateFormat = java.text.DateFormat[dateType.toUpperCase()];
|
||
for each (let timeType in ["short", "medium", "long", "full"]) {
|
||
let timeFormat = java.text.DateFormat[timeType.toUpperCase()];
|
||
let sdf = java.text.DateFormat.getDateTimeInstance(dateFormat, timeFormat, locale);
|
||
let pattern = sdf.toPattern();
|
||
if (patterns[pattern]) {
|
||
continue;
|
||
}
|
||
patterns[pattern] = true;
|
||
sdf.setTimeZone(timeZone);
|
||
result.push([encodeForm(pattern), sdf.format(now)]);
|
||
}
|
||
}
|
||
result.push([encodeForm(Date.ISOFORMAT), now.format(Date.ISOFORMAT, locale, timeZone)]);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} value
|
||
* @param {Object} param
|
||
* @param {Object} defaultValue
|
||
* @returns {Object} The value argument if truthy, the defaultValue argument
|
||
* otherwise
|
||
*/
|
||
function default_filter(value, param, defaultValue) {
|
||
return value || defaultValue;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Date} value
|
||
* @param {Object} param
|
||
* @returns {String} The age string of a date
|
||
*/
|
||
function age_filter(value, param) {
|
||
if (!value || value.constructor !== Date) {
|
||
return value;
|
||
}
|
||
return value.getAge()
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} text
|
||
* @param {String} param
|
||
* @param {Object} url
|
||
* @returns {String} The rendered link element
|
||
* @see renderLink
|
||
*/
|
||
function link_filter(text, param, url) {
|
||
if (text) {
|
||
url || (url = text);
|
||
res.push();
|
||
renderLink(param, url, text);
|
||
return res.pop();
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} string
|
||
* @param {Object} param
|
||
* @param {String} pattern
|
||
* @param {String} type
|
||
* @returns {String} The formatted string
|
||
*/
|
||
function format_filter(value, param, pattern, type) {
|
||
if (!value && value !== 0) {
|
||
return;
|
||
}
|
||
var f = global["format" + value.constructor.name];
|
||
if (f && f.constructor === Function) {
|
||
return f(value, pattern || param.pattern, type);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} input
|
||
* @param {Object} param
|
||
* @param {Number} limit
|
||
* @param {String} clipping
|
||
* @param {String} delimiter
|
||
* @returns {String} The clipped input
|
||
*/
|
||
function clip_filter(input, param, limit, clipping, delimiter) {
|
||
var len = 0;
|
||
if (input) {
|
||
len = input.length;
|
||
input = input.stripTags();
|
||
}
|
||
input || (input = ngettext("({0} character)", "({0} characters)", len));
|
||
limit || (limit = 20);
|
||
clipping || (clipping = "...");
|
||
delimiter || (delimiter = "\\s");
|
||
return String(input || "").head(limit, clipping, delimiter);
|
||
}
|
||
|
||
// FIXME:
|
||
/**
|
||
*
|
||
* @param {String} rss
|
||
* @returns {String} The fixed RSS string
|
||
*/
|
||
function fixRssText(rss) {
|
||
var re = new RegExp("<img src\\s*=\\s*\"?([^\\s\"]+)?\"?[^>]*?(alt\\s*=\\s*\"?([^\"]+)?\"?[^>]*?)?>", "gi");
|
||
rss = rss.replace(re, "[<a href=\"$1\" title=\"$3\">Image</a>]");
|
||
return rss;
|
||
}
|
||
|
||
// FIXME:
|
||
/**
|
||
*
|
||
*/
|
||
function countUsers() {
|
||
app.data.activeUsers = new Array();
|
||
var l = app.getActiveUsers();
|
||
for (var i in l)
|
||
app.data.activeUsers[app.data.activeUsers.length] = l[i];
|
||
l = app.getSessions();
|
||
app.data.sessions = 0;
|
||
for (var i in l) {
|
||
if (!l[i].user)
|
||
app.data.sessions++;
|
||
}
|
||
app.data.activeUsers.sort();
|
||
return;
|
||
}
|
||
|
||
// FIXME:
|
||
/**
|
||
* @ignore
|
||
* @param {Object} src
|
||
*/
|
||
function doWikiStuff (src) {
|
||
// robert, disabled: didn't get the reason for this:
|
||
// var src= " "+src;
|
||
if (src == null || !src.contains("<*"))
|
||
return src;
|
||
|
||
// do the Wiki link thing, <*asterisk style*>
|
||
var regex = new RegExp ("<[*]([^*]+)[*]>");
|
||
regex.ignoreCase=true;
|
||
|
||
var text = "";
|
||
var start = 0;
|
||
while (true) {
|
||
var found = regex.exec (src.substring(start));
|
||
var to = found == null ? src.length : start + found.index;
|
||
text += src.substring(start, to);
|
||
if (found == null)
|
||
break;
|
||
var name = ""+(new java.lang.String (found[1])).trim();
|
||
var item = res.handlers.site.topics.get (name);
|
||
if (item == null && name.lastIndexOf("s") == name.length-1)
|
||
item = res.handlers.site.topics.get (name.substring(0, name.length-1));
|
||
if (item == null || !item.size())
|
||
text += format(name)+" <small>[<a href=\""+res.handlers.site.stories.href("create")+"?topic="+escape(name)+"\">define "+format(name)+"</a>]</small>";
|
||
else
|
||
text += "<a href=\""+item.href()+"\">"+name+"</a>";
|
||
start += found.index + found[1].length+4;
|
||
}
|
||
return text;
|
||
}
|
||
|
||
// FIXME: Rewrite with jala.ListRenderer?
|
||
/**
|
||
*
|
||
* @param {HopObject|Array} collection
|
||
* @param {Function|Skin} funcOrSkin
|
||
* @param {Number} itemsPerPage
|
||
* @param {Number} pageIdx
|
||
* @returns {String} The rendered list
|
||
*/
|
||
function renderList(collection, funcOrSkin, itemsPerPage, pageIdx) {
|
||
var currIdx = 0, item;
|
||
var isArray = collection instanceof Array;
|
||
var stop = size = isArray ? collection.length : collection.size();
|
||
|
||
if (itemsPerPage) {
|
||
var totalPages = Math.ceil(size/itemsPerPage);
|
||
if (isNaN(pageIdx) || pageIdx > totalPages || pageIdx < 0) {
|
||
pageIdx = 0;
|
||
}
|
||
currIdx = pageIdx * itemsPerPage;
|
||
stop = Math.min(currIdx + itemsPerPage, size);
|
||
}
|
||
|
||
var isFunction = (funcOrSkin instanceof Function) ? true : false;
|
||
res.push();
|
||
while (currIdx < stop) {
|
||
item = isArray ? collection[currIdx] : collection.get(currIdx);
|
||
isFunction ? funcOrSkin(item) : item.renderSkin(funcOrSkin);
|
||
currIdx += 1;
|
||
}
|
||
return res.pop();
|
||
}
|
||
|
||
// FIXME: Rewrite using jala.ListRenderer or rename (eg. renderIndex)
|
||
/**
|
||
*
|
||
* @param {HopObject|Array|Number} collectionOrSize
|
||
* @param {String} url
|
||
* @param {Number} itemsPerPage
|
||
* @param {Number} pageIdx
|
||
* @returns {String} The rendered index
|
||
*/
|
||
function renderPager(collectionOrSize, url, itemsPerPage, pageIdx) {
|
||
// Render a single item for the navigation bar
|
||
var renderItem = function(text, cssClass, url, page) {
|
||
var param = {"class": cssClass};
|
||
if (!url) {
|
||
param.text = text;
|
||
} else {
|
||
if (url.contains("?"))
|
||
param.text = html.linkAsString({href: url + "&page=" + page}, text);
|
||
else
|
||
param.text = html.linkAsString({href: url + "?page=" + page}, text);
|
||
}
|
||
renderSkin("$Global#pagerItem", param);
|
||
return;
|
||
}
|
||
|
||
var maxItems = 10;
|
||
var size = 0;
|
||
if (collectionOrSize instanceof Array) {
|
||
size = collectionOrSize.length;
|
||
} else if (collectionOrSize instanceof HopObject) {
|
||
size = collectionOrSize.size();
|
||
} else if (!isNaN(collectionOrSize)) {
|
||
size = parseInt(collectionOrSize, 10);
|
||
}
|
||
var lastPageIdx = Math.ceil(size/itemsPerPage)-1;
|
||
// If there's just one page no navigation will be rendered
|
||
if (lastPageIdx <= 0) {
|
||
return null;
|
||
}
|
||
|
||
// Initialize the parameter object
|
||
var param = {};
|
||
var pageIdx = parseInt(pageIdx, 10);
|
||
// Check if the passed index is correct
|
||
if (isNaN(pageIdx) || pageIdx > lastPageIdx || pageIdx < 0) {
|
||
pageIdx = 0;
|
||
}
|
||
param.display = ((pageIdx * itemsPerPage) + 1) + "-" +
|
||
(Math.min((pageIdx * itemsPerPage) + itemsPerPage, size));
|
||
param.total = size;
|
||
|
||
// Render the navigation-bar
|
||
res.push();
|
||
(pageIdx > 0) && renderItem("[–]", "pageNavItem", url, pageIdx-1);
|
||
var offset = Math.floor(pageIdx / maxItems) * maxItems;
|
||
(offset > 0) && renderItem("[..]", "pageNavItem", url, offset-1);
|
||
var currPage = offset;
|
||
var stop = Math.min(currPage + maxItems, lastPageIdx+1);
|
||
while (currPage < stop) {
|
||
if (currPage === pageIdx) {
|
||
renderItem("[" + (currPage +1) + "]", "pageNavSelItem");
|
||
} else {
|
||
renderItem("[" + (currPage +1) + "]", "pageNavItem", url, currPage);
|
||
}
|
||
currPage += 1;
|
||
}
|
||
if (currPage < lastPageIdx) {
|
||
renderItem("[..]", "pageNavItem", url, offset + maxItems);
|
||
}
|
||
if (pageIdx < lastPageIdx) {
|
||
renderItem("[+]", "pageNavItem", url, pageIdx +1);
|
||
}
|
||
param.pager = res.pop();
|
||
return renderSkinAsString("$Global#pager", param);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} plural
|
||
* @returns {String} The english singular form of the input
|
||
*/
|
||
function singularize(plural) {
|
||
if (plural.endsWith("ies")) {
|
||
return plural.substring(0, plural.length-3) + "y";
|
||
}
|
||
return plural.substring(0, plural.length-1);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} singular
|
||
* @returns {String} The english plural form of the input
|
||
*/
|
||
function pluralize(singular) {
|
||
if (singular.endsWith("y")) {
|
||
return singular.substring(0, singular.length-1) + "ies";
|
||
}
|
||
return singular + "s";
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Number} millis
|
||
*/
|
||
var wait = function(millis) {
|
||
millis || (millis = Date.ONESECOND);
|
||
var now = new Date;
|
||
while (new Date - now < millis) {
|
||
void null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {String} name
|
||
* @param {Object} extra
|
||
*/
|
||
var registerExtra = function(name, extra) {
|
||
app.data.extras[name] = extra;
|
||
if (extra._macro) {
|
||
app.data.extras[name + "_macro"] = function() {
|
||
return extra._macro.apply(this, arguments);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {Object} param
|
||
* @param {String} type
|
||
*/
|
||
function version_macro(param, type) {
|
||
var version = Root.VERSION;
|
||
var result = version[type || "default"];
|
||
return result || version;
|
||
}
|