// The Antville Project
// http://code.google.com/p/antville
//
// Copyright 2001–2014 by the Workers of Antville.
//
// 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.
/**
* @fileOverview Defines global variables and functions.
*/
String.ELLIPSIS = '…';
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');
// Adding i18n message files as repositories
(function() {
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!
/**
* Helma’s built-in application-wide in-memory store.
* @name app.data
* @namespace
*/
/**
* Temporary in-memory store of site callbacks.
* They will be invoked asynchronously by an Admin method.
* @see Admin.invokeCallbacks
* @see scheduler
* @name app.data.callbacks
*/
app.data.callbacks || (app.data.callbacks = []);
/**
* Temporary in-memory store of LogEntry instances.
* They will be made persistent asynchronously by an Admin method.
* @see Admin.commitEntries
* @see scheduler
* @name app.data.entries
* @type Array
*/
app.data.entries || (app.data.entries = []);
/**
* In-memory registry of Claustra instances.
* Claustra are defined in the “claustra” dir.
* @name app.data.claustra
* @type Array
*/
app.data.claustra || (app.data.claustra = []);
/**
* In-memory e-mail message queue.
* They will be sent asynchronously by an Admin method.
* @see helma.mail.flushQueue
* @see scheduler
* @name app.data.mails
* @type Array
*/
app.data.mails || (app.data.mails = []);
/**
* In-memory store of remote requests for counting story hits.
* They will be made persistent asynchronously by an Admin method.
* @see Admin.commitRequests
* @see scheduler
* @name app.data.requests
* @type Array
*/
app.data.requests || (app.data.requests = {});
/**
* The helma.File prototype is defined as a module.
* @name helma.File
* @namespace
*/
/**
* Helper method for recursively copying a directory and its files.
* @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;
}
/**
* The helma.Mail prototype is defined in a module.
* @name helma.Mail
* @namespace
*/
/**
* Add an e-mail message to the mail queue for later sending.
* @see app.data.mails
* @returns {Number} The number of mails waiting in the queue
*/
helma.Mail.prototype.queue = function() {
return app.data.mails.push(this);
}
/**
* Try to send and remove every mail instance collected in the mail queue.
* @see app.data.mails
*/
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;
}
/**
* The jala.i18n namespace is defined in a module.
* @name jala.i18n
* @namespace
*/
jala.i18n.setLocaleGetter(function() {
return (res.handlers.site || root).getLocale();
});
/**
* The date format used in SQL queries and commands.
* @constant
* @type String
*/
var SQLDATEFORMAT = 'yyyy-MM-dd HH:mm:ss';
/**
* Regular Expression according to Jala’s HopObject.getAccessName().
* @constant
* @type RegExp
*/
var NAMEPATTERN = /[\/+\\]/;
/**
* Shortcut for a function with empty body.
* Used e.g. in the disableMacro() method.
* @see disableMacro
* @function
*/
var idle = new Function;
/**
* An instance of Helma’s HTML rendering module.
* @type helma.Html
*/
var html = new helma.Html();
/**
* An instance of the LESS parser.
* @type less.Parser
*/
var lessParser = new less.Parser();
/**
* A collection of Java classes and namespaces required for parsing and generating RSS.
* @type Object
*/
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
);
/**
* A simple and hackish implementation of the console instance of some browsers.
* @namespace
*/
var console = function (type) {
/**
* Convenience method for bridging log output from the server to the client.
* @methodOf console
* @param {String} text This text will be displayed in the browser’s console (if available).
*/
return function(text /*, text, … */) {
var now = formatDate(new Date, 'yyyy/MM/dd HH:mm:ss');
var argString = Array.prototype.join.call(arguments, String.SPACE);
var shellColors = {
debug: '\u001B[34m',
error: '\u001B[35m',
info: '\u001B[0m',
log: '\u001B[0m',
warn: '\u001B[31m'
};
writeln(shellColors[type] + '[' + now + '] [' + type.toUpperCase() + '] [console] ' + argString + '\u001B[0m');
if (typeof res !== 'undefined') {
res.debug('');
}
}
};
console.log = console('log');
console.debug = console('debug');
console.info = console('info');
console.warn = console('warn');
console.error = console('error');
/**
* The startup handler Helma is calling automatically shortly after the application has started.
*/
function onStart() {
if (typeof root === 'undefined') {
app.logger.error('Error in database configuration: no root site found.');
return;
}
// Load add-ons aka claustra
Claustra.load();
// This is necessary once to be sure that aspect-oriented code will be applied
HopObject.prototype.onCodeUpdate && HopObject.prototype.onCodeUpdate();
return;
}
/**
* This handler is called by Helma automatically before the application is stopped.
*/
function onStop() { /* Currently empty, just to avoid annoying log message */ }
/**
* Helper method to simultaneously define constants and a corresponding array of localized display names.
* @param {HopObject} ctor The desired prototype constructor the constants should be defined for.
* @returns {Function}
*/
function defineConstants(ctor /*, arguments */) {
var constants = [], name;
for (var i=1; i is value then <% yes suffix=! %> else 'no :(' %>;
* 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;
}
/**
* Renders the current date and time.
* @see formatDate
* @param {Object} param The default Helma macro parameter object
* @param {String} [format] A date format string
* @returns {String} The formatted current date string
*/
var now_macro = function(param, format) {
return formatDate(new Date, format || param.format);
};
/**
* Renders a link.
* @see renderLink
* @returns {String} The rendered HTML link element
*/
function link_macro() {
return renderLink.apply(this, arguments);
}
/**
*
* @param {Object} param
* @param {HopObject} object
*/
function count_macro(param, object) {
if (object && object.size && object.size instanceof Function) {
res.write(object.size());
}
return;
}
/**
* Renders a skin from within a skin.
* @see HopObject#skin_macro
* @returns {String} The rendered skin
*/
// 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);
}
/**
* Renders a breadcrumbs navigation from the current HopObject path.
* @param {Object} param The default Helma macro parameter object
* @param {String} [delimiter=' : '] The string visually separating two navigation items
*/
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 Story #1810 in preview skin
* <% story blog/1971 url %> URL of the story of site “blog”
*/
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:
var skin = param.skin ? 'Story#' + param.skin : '$Story#embed';
story.renderSkin(skin);
}
return;
}
/**
* Renders the URL or an arbitrary skin of a file.
* @param {Object} param The default Helma macro parameter object
* @param {String} [param.skin='main'] The name of a file skin
* @param {String} id The id or path of the desired file
* @param {String} [mode] Currently only possible value is 'url'
* @example <% file 1810 url %> URL of file #1810
* <% file blog/text.pdf skin=preview %> File in site “blog” using preview skin
* <% file /image.raw %> Static file of root site
*/
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) {
if (res.contentType === 'text/html' && !id.contains('/')) {
res.handlers.site.link_macro({
'class': 'uk-icon-button uk-icon-file-o',
'data-uk-tooltip': "{pos: 'right'}",
title: gettext('Create missing file'),
}, 'files/create?name=' + encodeURIComponent(id), ' ');
}
return;
}
if (mode === 'url') {
res.write(file.getUrl());
} else {
if (mode === 'player') {
param.skin = file.contentType.split('/')[0];
}
file.renderSkin('File#' + (param.skin || 'main'));
}
return;
}
/**
* Renders the URL, a thumbnail or an HTML element of an image.
* @see Image#thumbnail_macro
* @see Image#render_macro
* @param {Object} param The default Helma macro parameter object
* @param {String} id The id or path of the desired image
* @param {String} [mode] Either of 'url' or 'thumbnail'
*/
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) {
if (res.contentType === 'text/html' && !id.contains('/')) {
res.handlers.site.link_macro({
'class': 'uk-icon-button uk-icon-picture-o',
'data-uk-tooltip': "{pos: 'right'}",
title: gettext('Create missing image'),
}, 'images/create?name=' + encodeURIComponent(id), ' ');
}
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
if (mode === 'popup') {
param.onclick = 'javascript: openPopup(\'' + url + '\', ' + image.width + ', ' + image.height + '); return false;';
}
image.thumbnail_macro(param);
html.closeTag('a');
break;
case 'box':
// Default Images do not provide the renderSkin() method
if (image.renderSkin) {
image.renderSkin(param.skin || '$Image#embed');
}
break;
default:
image.render_macro(param);
}
return;
}
/**
* Renders the URL, a link or the visual representation of a poll.
* @param {Object} param The default Helma macro parameter object
* @param {String} id The id or path of the desired poll
* @param {String} mode Either of 'url' or 'link'
*/
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:
poll.renderSkin('$Poll#embed');
}
return;
}
/**
* The “swiss army knife” list macro. Lists collections of HopObjects.
* There is hardly a thing it cannot do… but it’s kind of messy, though.
* @param {Object} param The default Helma macro parameter object
* @param {String} [param.skin=preview] The name of a skin suitable for the collection
* @param {String} id The identifier of the desired collection
* @param {Number} [limit=25] The maximum amount of items listed
* @example <% list sites %>
* <% list updates 10 %>
* <% list blog/comments %>
* <% list featured skin=promotion %>
* <% list images %>
* <% list postings %>
* <% list stories %>
* <% list tags %>
*/
function list_macro(param, id, limit) {
if (!id) {
return;
}
var skin, collection;
var max = Math.min(limit || 25, 100);
if (id === 'sites') {
collection = root.sites.list(0, max);
skin = 'Site#preview';
} else if (id === 'updates') {
collection = root.updates.list(0, max).map(function (site) {
return site.stories.union.get(0);
});
skin = 'Story#preview';
} else {
var site, type;
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 'macros':
res.handlers.site.stories.renderSkin('$Stories#macros');
return;
case 'postings':
var 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;
}
}
for each (var item in collection) {
item && item.renderSkin(param.skin || skin);
}
return;
}
/**
* Defines and renders a value.
* This works like a variable that can be set in one skin and rendered in another –
* which must be rendered later than the one setting the variable.
* @param {Object} param The default Helma macro parameter object.
* @param {String} name The name of the value.
* @param {String} [value] The desired value.
* If no value is given, the current value will be rendered.
* @example <% value foo=bar %> Defines res.meta.values.foo = bar
* @example <% value foo %> Renders the value of res.meta.value.foo
*/
function value_macro(param, name, value) {
if (!name) {
return;
}
name = name.toLowerCase();
if (!value) {
return res.meta.values[name];
}
res.meta.values[name] = value;
return;
}
function href_macro(param) {
var href = path.href(req.action === 'main' ? String.EMPTY : req.action);
if (!href.startsWith('http')) {
href = req.servletRequest.rootURL + href;
}
return res.write(href);
}
/**
* Renders either a skin or the URL of a random site, story or image.
* The corresponding story and image collections will be retrieved either from res.handlers.site or
* from the prefixed “type” argument (e.g. “mySite/story”).
* Furthermore, both collections can be reduced to a specific tag or gallery, resp.
* @param {Object} param The default Helma macro parameter object.
* @param {String} [param.skin = 'preview'] The name of the skin to render in default output mode.
* @param {String} [param.tag] Reduce the story collection to stories with the specified tag.
* @param {String} [param.gallery] Reduce the image collection to images from the specified gallery.
* @param {String} type The type of object to render. Either of “site”, “story” or “image”.
* It can be prepended by a site name delimited by a slash: “mySite/image”.
* @param {String} [mode] Set the output mode. Currently, only “url” is supported.
* @example <% random site skin=preview %> Renders the preview skin of a random site.
* <% random story tag=essay url %> Renders the URL of a random story tagged with “essay”.
* <% random foo/image gallery=cat %> Renders the default skin of a random image in the gallery “cat“ of the site “foo”.
*/
function random_macro(param, type, mode) {
var getRandom = function(n) {
return Math.floor(Math.random() * n);
};
var site = res.handlers.site;
if (type === 'site') {
site = root.sites.get(getRandom(root.sites.size()));
mode === 'url' ? res.write(site.href()) :
site.renderSkin(param.skin || 'Site#preview');
return;
}
var parts = type.split('/');
if (parts.length > 1) {
site = root.sites.get(parts[0] || 'www');
type = parts[1];
} else {
type = parts[0];
}
if (!site) {
return;
}
switch (type) {
case 'story':
case 'stories':
var stories = param.tag ? site.stories.tags.get(param.tag) :
site.stories.featured;
var story = stories && stories.get(getRandom(stories.size()));
if (story) {
param.tag && (story = story.tagged);
mode === 'url' ? res.write(story.href()) :
story.renderSkin(param.skin || 'Story#preview');
}
break;
case 'image':
case 'images':
var images = param.gallery ? site.images.galleries.get(param.gallery) :
site.images;
var image = images && images.get(getRandom(images.size()));
if (image) {
param.gallery && (image = image.tagged);
mode === 'url' ? res.write(image.href()) :
image.renderSkin(param.skin || 'Image#preview');
}
break;
}
return;
}
/**
* Renders the Antville version string.
* @param {Object} param The default Helma macro parameter object.
* @param {String} [type = 'default'] The type of version string.
* @see Root.VERSION
*/
function version_macro(param, type) {
var version = Root.VERSION;
var result = version[type || 'default'];
return result || version;
}
/**
* A simple Helma macro filter returning one of two possible values depending on which one is truthy.
* @param {Object} value The original (desired) value.
* @param {Object} param The default Helma macro parameter object.
* @param {Object} defaultValue The fallback value for use if the original value should be untruthy.
* @returns {Object} The value argument if truthy, the defaultValue argument otherwise.
*/
function default_filter(value, param, defaultValue) {
return value || defaultValue;
}
/**
* Helma macro filter wrapping the {@link Date#getAge} method.
* @see Date#getAge
* @param {Date} value The original date.
* @param {Object} param The default Helma macro parameter object.
* @returns {String} The resulting age string of the original date.
*/
function age_filter(value, param) {
if (!value || value.constructor !== Date) {
return value;
}
return value.getAge()
}
/**
* Helma macro filter wrapping the {@link renderLink} method.
* @param {String} text The link text.
* @param {String} param The default Helma macro parameter object.
* @param {Object} [url = text] The link 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;
}
/**
* Helma macro filter wrapping the global formatting methods.
* @see formatNumber
* @see formatDate
* @param {Object} value The original value.
* @param {Object} param The default Helma macro parameter object.
* @param {String} pattern A formatting pattern suitable for the formatting method.
* @param {String} [type] Deprecated.
* @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;
}
/**
* Macro filter for clipping output.
* @param {String} input The original input.
* @param {Object} param The default Helma macro parameter object.
* @param {Number} [limit = 20] The maximum amount of text parts to be displayed.
* @param {String} [clipping = '...'] The replacement for the clipped portions of the text.
* @param {String} [delimiter = '\\s'] The regular expression string used to split the text into parts.
* @returns {String} The clipped result.
*/
function clip_filter(input, param, limit, clipping, delimiter) {
var len = 0;
if (input) {
len = input.length;
input = input.stripTags();
}
if (!input && param['default'] === null) {
return ngettext('({0} character)', '({0} characters)', len);
}
limit || (limit = 20);
clipping || (clipping = String.ELLIPSIS);
delimiter || (delimiter = '\\s');
return String(input || '').head(limit, clipping, delimiter);
}
function json_filter(value, param) {
return JSON.stringify(value);
}
function script_filter(value, param) {
// Remove