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