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