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