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 the Site prototype.
 27  */
 28 
 29 this.handleMetadata("archiveMode");
 30 this.handleMetadata("commentMode");
 31 this.handleMetadata("configured");
 32 this.handleMetadata("locale");
 33 this.handleMetadata("longDateFormat");
 34 this.handleMetadata("notificationMode");
 35 this.handleMetadata("notifiedOfBlocking");
 36 this.handleMetadata("notifiedOfDeletion");
 37 this.handleMetadata("closed");
 38 this.handleMetadata("pageSize");
 39 this.handleMetadata("pageMode");
 40 this.handleMetadata("shortDateFormat");
 41 this.handleMetadata("tagline");
 42 this.handleMetadata("timeZone");
 43 this.handleMetadata("title"),
 44 this.handleMetadata("callbackMode");
 45 this.handleMetadata("callbackUrl");
 46 this.handleMetadata("spamfilter");
 47 
 48 /**
 49  * @function
 50  * @returns {String[]}
 51  * @see defineConstants
 52  */
 53 Site.getStatus = defineConstants(Site, "blocked", "regular", "trusted");
 54 /**
 55  * @function
 56  * @returns {String[]}
 57  * @see defineConstants
 58  */
 59 Site.getModes = defineConstants(Site, "closed", "restricted", "public", "open");
 60 /**
 61  * @function
 62  * @returns {String[]}
 63  * @see defineConstants
 64  */
 65 Site.getPageModes = defineConstants(Site, "stories"); //, "days");
 66 /**
 67  * @function
 68  * @returns {String[]}
 69  * @see defineConstants
 70  */
 71 Site.getCommentModes = defineConstants(Site, "disabled", "enabled");
 72 /**
 73  * @function
 74  * @returns {String[]}
 75  * @see defineConstants
 76  */
 77 Site.getArchiveModes = defineConstants(Site, "closed", "public");
 78 /**
 79  * @function
 80  * @returns {String[]}
 81  * @see defineConstants
 82  */
 83 Site.getNotificationModes = defineConstants(Site, "Nobody", 
 84       "Owner", "Manager", "Contributor", "Subscriber" );
 85 /**
 86  * @function
 87  * @returns {String[]}
 88  * @see defineConstants
 89  */
 90 Site.getCallbackModes = defineConstants(Site, "disabled", "enabled");
 91 
 92 /**
 93  * 
 94  * @param {Site} site
 95  */
 96 Site.remove = function(site) {
 97    HopObject.remove(site.members);
 98    HopObject.remove(site.stories);
 99    HopObject.remove(site.images);
100    HopObject.remove(site.files);
101    HopObject.remove(site.polls);
102    site.layout && Layout.remove.call(site.layout);
103    site.getStaticFile().removeDirectory();
104    site.remove();
105    // FIXME: There is a problem in the log structure for a deleted site
106    root.admin.log(root, "Removed site " + site.name);
107    return;
108 }
109 
110 /**
111  * 
112  * @param {String} name
113  * @returns {Site}
114  */
115 Site.getByName = function(name) {
116    return root.get(name);
117 }
118 
119 /**
120  * 
121  * @param {String} mode
122  * @returns {Boolean}
123  */
124 Site.require = function(mode) {
125    var modes = [Site.CLOSED, Site.RESTRICTED, Site.PUBLIC, Site.OPEN];
126    return modes.indexOf(res.handlers.site.mode) >= modes.indexOf(mode);
127 }
128 
129 /**
130  * A Site object is the basic container of Antville.
131  * @name Site
132  * @constructor
133  * @param {String} name A unique identifier also used in the URL of a site
134  * @param {String} title An arbitrary string branding a site
135  * @property {Tag[]} $tags
136  * @property {Archive} archive
137  * @property {String} archiveMode The way the archive of a site is displayed
138  * @property {String} commentMode The way comments of a site are displayed
139  * @property {Date} created The date and time of site creation
140  * @property {User} creator A reference to a user who created a site
141  * @property {Tags} galleries
142  * @property {Files} files
143  * @property {Images} images
144  * @property {Layout} layout
145  * @property {String} locale The place and language settings of a site
146  * @property {String} longDateFormat The long date format string
147  * @property {Members} members
148  * @property {Metadata} metadata
149  * @property {String} mode The access level of a site
150  * @property {Date} modified The date and time when a site was last modified
151  * @property {User} modifier A reference to a user who modified a site
152  * @property {String} notificationMode The way notifications are sent from a site
153  * @property {String} pageMode The way stories of a site are displayed
154  * @property {Number} pageSize The amount of stories to be displayed simultaneously
155  * @property {Polls} polls
156  * @property {String} shortDateFormat The short date format string
157  * @property {String} status The trust level of a site
158  * @property {Stories} stories
159  * @property {String} tagline An arbitrary text describing a site
160  * @property {Tags} tags
161  * @property {String} timeZone The time and date settings of a site
162  * @extends HopObject
163  */
164 Site.prototype.constructor = function(name, title) {
165    var now = new Date;
166    var locale = root.getLocale();
167    var user = session.user || new HopObject;
168 
169    this.map({
170       name: name,
171       title: title || name,
172       created: now,
173       creator: user,
174       modified: now,
175       modifier: user,
176       status: user.status === User.PRIVILEGED ? Site.TRUSTED : user.status,
177       mode: Site.CLOSED,
178       tagline: String.EMPTY,
179       callbackEnabled: false,
180       commentMode: Site.OPEN,
181       archiveMode: Site.PUBLIC,
182       notificationMode: Site.DISABLED,
183       pageMode: Site.DAYS,
184       pageSize: 3,
185       locale: locale.toString(),
186       timeZone: root.getTimeZone().getID(),
187       longDateFormat: LONGDATEFORMAT,
188       shortDateFormat: SHORTDATEFORMAT
189    });
190 
191    return this;
192 }
193 
194 /**
195  * 
196  * @param {String} action
197  * @returns {Boolean}
198  */
199 Site.prototype.getPermission = function(action) {
200    switch (action) {
201       case "backup.js":
202       case "main.js":
203       case "main.css":
204       case "error":
205       case "notfound":
206       case "robots.txt":
207       case "search":
208       case "search.xml":
209       case "user.js":
210       return true;
211 
212       case ".":
213       case "main":
214       case "comments.xml":
215       case "rss.xml":
216       case "rss.xsl":
217       case "stories.xml":
218       return Site.require(Site.PUBLIC) ||
219             (Site.require(Site.RESTRICTED) && 
220             Membership.require(Membership.CONTRIBUTOR)) ||
221             (Site.require(Site.CLOSED) &&
222             Membership.require(Membership.OWNER)) ||
223             User.require(User.PRIVILEGED);
224 
225       case "edit":
226       case "export":
227       case "referrers":
228       return Membership.require(Membership.OWNER) ||
229             User.require(User.PRIVILEGED);
230 
231       case "subscribe":
232       return Site.require(Site.PUBLIC) &&
233             !Membership.require(Membership.SUBSCRIBER);
234 
235       case "unsubscribe":
236       if (User.require(User.REGULAR)) {
237          var membership = Membership.getByName(session.user.name);
238          return membership && !membership.require(Membership.OWNER);
239       }
240    }
241    return false;
242 }
243 
244 Site.prototype.main_action = function() {
245    res.data.body = this.renderSkinAsString("Site#main");
246    res.data.title = this.title;
247    this.renderSkin("Site#page");
248    this.log();
249    return;
250 }
251 
252 Site.prototype.edit_action = function() {
253    if (req.postParams.save) {
254       try {
255          this.update(req.postParams);
256          res.message = gettext("The changes were saved successfully.");
257          res.redirect(this.href(req.action));
258       } catch (ex) {
259          res.message = ex;
260          app.log(ex);
261          /*writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
262          var e = new Packages.org.mozilla.javascript.EvaluatorException(ex);
263          e.fillInStackTrace();
264          res.debug(e.getScriptStackTrace());
265          res.debug(e.printStackTrace(java.lang.System.out));
266          var trace = e.getStackTrace();
267          writeln(trace.toString());
268          for (var i in ex)
269          app.log(i + ": " + ex[i]);*/
270       }
271    }
272 
273    res.data.action = this.href(req.action);
274    res.data.title = gettext("Preferences of {0}", this.title);
275    res.data.body = this.renderSkinAsString("$Site#edit");
276    this.renderSkin("Site#page");
277    return;
278 }
279 
280 /**
281  * 
282  * @param {String} name
283  * @returns {Object}
284  */
285 Site.prototype.getFormOptions = function(name) {
286    switch (name) {
287       case "archiveMode":
288       return Site.getArchiveModes();
289       case "commentMode":
290       return Site.getCommentModes();
291       case "locale":
292       return getLocales();
293       case "layout":
294       return this.getLayouts();
295       case "longDateFormat":
296       return getDateFormats("long");
297       case "mode":
298       return Site.getModes();
299       case "notificationMode":
300       return Site.getNotificationModes();
301       case "pageMode":
302       return Site.getPageModes();
303       case "status":
304       return Site.getStatus();
305       case "shortDateFormat":
306       return getDateFormats("short");
307       case "timeZone":
308       return getTimeZones();
309       case "callbackMode":
310       return Site.getCallbackModes();
311       default:
312       return HopObject.prototype.getFormOptions.apply(this, arguments);
313    }
314 }
315 
316 /**
317  * 
318  * @param {Object} data
319  */
320 Site.prototype.update = function(data) {
321    if (this.isTransient()) {
322       if (!data.name) {
323          throw Error(gettext("Please enter a name for your new site."));
324       } else if (data.name.length > 30) {
325          throw Error(gettext("The chosen name is too long. Please enter a shorter one."));
326       } else if (/(\/|\\)/.test(data.name)) {
327          throw Error(gettext("A site name may not contain any (back)slashes."));
328       } else if (data.name !== root.getAccessName(data.name)) {
329          throw Error(gettext("There already is a site with this name."));
330       }
331       this.layout = new Layout(this);
332       this.name = data.name;
333       this.title = data.title || data.name;
334       return;
335    }
336 
337    this.map({
338       title: stripTags(data.title) || this.name,
339       tagline: data.tagline,
340       mode: data.mode || Site.PRIVATE,
341       callbackUrl: data.callbackUrl,
342       callbackMode: data.callbackMode || Site.DISABLED,
343       pageMode: data.pageMode || Site.DAYS,
344       pageSize: parseInt(data.pageSize, 10) || this.pageSize || 3,
345       commentMode: data.commentMode || Site.DISABLED,
346       archiveMode: data.archiveMode || Site.CLOSED,
347       notificationMode: data.notificationMode || Site.DISABLED,
348       timeZone: data.timeZone,
349       longDateFormat: data.longDateFormat,
350       shortDateFormat: data.shortDateFormat,
351       locale: data.locale,
352       spamfilter: data.spamfilter
353    });
354 
355    this.configured = new Date;
356    this.modifier = session.user;
357    this.clearCache();
358    return;
359 }
360 
361 Site.prototype.main_css_action = function() {
362    res.dependsOn(this.modified);
363    res.dependsOn(Skin("Site", "values").getSource());
364    res.dependsOn(Skin("Site", "stylesheet").getSource());
365    res.digest();
366    res.contentType = "text/css";
367    this.renderSkin("Site#stylesheet");
368    return;
369 }
370 
371 Site.prototype.main_js_action = function() {
372    res.contentType = "text/javascript";
373    for each (script in ["jquery-1.2.3.min.js", "antville-1.2.js"]) {
374       this.renderSkin("$Site#include", {href: root.getStaticUrl(script)});
375    }
376    this.renderSkin("$Site#include", {href: this.href("user.js")});
377    return;
378 }
379 
380 Site.prototype.user_js_action = function() {
381    res.contentType = "text/javascript";
382    res.dependsOn(this.modified);
383    res.dependsOn(Skin("Site", "javascript").getSource());
384    res.digest();
385    this.renderSkin("Site#javascript");
386    return;  
387 }
388 
389 Site.prototype.backup_js_action = function() {
390    if (req.isPost()) {
391       session.data.backup = {};
392       for (var key in req.postParams) {
393          session.data.backup[key] = req.postParams[key];
394       }
395    }
396    return;
397 }
398 
399 Site.prototype.rss_xml_action = function() {
400    res.dependsOn(this.modified);
401    res.digest();
402    res.contentType = "text/xml";
403    res.write(this.getXml(this.stories.union));
404    return;
405 }
406 
407 Site.prototype.stories_xml_action = function() {
408    res.dependsOn(this.modified);
409    res.digest();
410    res.contentType = "text/xml";
411    res.write(this.getXml(this.stories.recent));
412    return;
413 }
414 
415 Site.prototype.comments_xml_action = function() {
416    res.dependsOn(this.modified);
417    res.digest();
418    res.contentType = "text/xml";
419    res.write(this.getXml(this.stories.comments));
420    return;
421 }
422 
423 Site.prototype.search_xml_action = function() {
424    return; // FIXME
425    res.contentType = "application/opensearchdescription+xml";
426    res.write(<OpenSearchDescription xmlns="http://antville.org/">
427       	<ShortName>Antville Search</ShortName>
428       	<Description>Search on Antville</Description>
429       	<Tags>antville search</Tags>
430       	<Image height="16" width="16" type="image/vnd.microsoft.icon">http://www.youtube.com/favicon.ico</Image>
431       	<Url type="text/html" template="http://antville.org/search?q={searchTerms}" />
432       	<Query role="example" searchTerms="cat" />
433          </OpenSearchDescription>);
434    return;   
435 }
436 
437 /**
438  * 
439  * @param {Story[]} collection
440  */
441 Site.prototype.getXml = function(collection) {
442    collection || (collection = this.stories.recent);
443    var now = new Date;
444    var feed = new rome.SyndFeedImpl();   
445    feed.setFeedType("rss_2.0");
446    feed.setLink(this.href());
447    feed.setTitle(this.title);
448    feed.setDescription(this.tagline || String.EMPTY);
449    feed.setLanguage(this.locale.replace("_", "-"));
450    feed.setPublishedDate(now);
451 
452    /*
453    var feedInfo = new rome.FeedInformationImpl();
454    var feedModules = new java.util.ArrayList();
455    feedModules.add(feedInfo);
456    feed.setModules(feedModules);
457    //feedInfo.setImage(new java.net.URL(this.getProperty("imageUrl")));
458    feedInfo.setSubtitle(this.tagline);
459    feedInfo.setSummary(this.description);
460    feedInfo.setAuthor(this.creator.name);
461    feedInfo.setOwnerName(this.creator.name);
462    //feedInfo.setOwnerEmailAddress(this.getProperty("email"));
463    */
464 
465    var entry, entryInfo, entryModules;
466    var enclosure, enclosures, keywords;
467    var entries = new java.util.ArrayList();
468    var description;
469 
470    var list = collection.constructor === Array ? 
471          collection : collection.list(0, 25);
472    for each (var item in list) {
473       entry = new rome.SyndEntryImpl();
474       item.title && entry.setTitle(item.title);
475       entry.setLink(item.href());
476       entry.setAuthor(item.creator.name);
477       entry.setPublishedDate(item.created);
478       if (item.text) {
479          // FIXME: Work-around for "story" handlers in comment skins
480          // (Obsolete as soon as "story" handlers are replaced with "this")
481          //res.handlers.story = item;
482          description = new rome.SyndContentImpl();
483          //description.setType("text/plain");
484          // FIXME: Work-around for org.jdom.IllegalDataException caused by some ASCII control characters 
485          description.setValue(item.renderSkinAsString("Story#rss").replace(/[\x00-\x1f^\x0a^\x0d]/g, function(c) {
486             return "&#" + c.charCodeAt(0) + ";";
487          }));
488          entry.setDescription(description);
489       }
490       entries.add(entry);
491       
492       /*
493       entryInfo = new rome.EntryInformationImpl();
494       entryModules = new java.util.ArrayList();
495       entryModules.add(entryInfo);
496       entry.setModules(entryModules);
497 
498       enclosure = new rome.SyndEnclosureImpl();
499       enclosure.setUrl(episode.getProperty("fileUrl"));
500       enclosure.setType(episode.getProperty("contentType"));
501       enclosure.setLength(episode.getProperty("filesize") || 0);
502       enclosures = new java.util.ArrayList();
503       enclosures.add(enclosure);
504       entry.setEnclosures(enclosures);
505 
506       entryInfo.setAuthor(entry.getAuthor());
507       entryInfo.setBlock(false);
508       entryInfo.setDuration(new rome.Duration(episode.getProperty("length") || 0));
509       entryInfo.setExplicit(false);
510       entryInfo.setKeywords(episode.getProperty("keywords"));
511       entryInfo.setSubtitle(episode.getProperty("subtitle"));
512       entryInfo.setSummary(episode.getProperty("description"));
513       */
514    }
515    feed.setEntries(entries);
516    
517    var output = new rome.SyndFeedOutput();
518    //output.output(feed, res.servletResponse.writer); return;
519    var xml = output.outputString(feed);
520    // FIXME: Ugly hack for adding PubSubHubbub and rssCloud elements to XML
521    xml = xml.replace("<rss", '<rss xmlns:atom="http://www.w3.org/2005/Atom"');
522    xml = xml.replace("<channel>", '<channel>\n    <cloud domain="rpc.rsscloud.org" port="5337" path="/rsscloud/pleaseNotify" registerProcedure="" protocol="http-post" />');
523    xml = xml.replace("<channel>", '<channel>\n    <atom:link rel="hub" href="' + getProperty("parss.hub") + '"/>'); 
524    return xml; //injectXslDeclaration(xml);
525 }
526 
527 Site.prototype.rss_xsl_action = function() {
528    res.charset = "UTF-8";
529    res.contentType = "text/xml";
530    renderSkin("Global#xslStylesheet");
531    return;
532 }
533 
534 Site.prototype.referrers_action = function() {
535    if (req.data.permanent && this.getPermission("edit"))  {
536       var urls = req.data.permanent_array;
537       res.write(this.metadata.get("spamfilter"));
538       for (var i in urls) {
539          res.write("\n");
540          res.write(urls[i].replace(/\?/g, "\\\\?"));
541       }
542       this.metadata.set("spamfilter", res.pop());
543       res.redirect(this.href(req.action));
544       return;
545    }
546    res.data.action = this.href(req.action);
547    res.data.title = gettext("Referrers in the last 24 hours of {0}", this.title);
548    res.data.body = this.renderSkinAsString("$Site#referrers");
549    this.renderSkin("Site#page");
550    return;
551 }
552 
553 Site.prototype.search_action = function() {
554    var search;
555    if (!(search = req.data.q) || !stripTags(search)) {
556       res.message = gettext("Please enter a query in the search form.");
557       res.data.body = this.renderSkinAsString("Site#search");
558    } else {
559       // Prepare search string for metadata: Get source and remove 
560       // '(new String("..."))' wrapper; finally, double all backslashes
561       search = String(search).toSource().slice(13, -3).replace(/(\\)/g, "$1$1");
562       var title = '%title:"%' + search + '%"%';
563       var text = '%text:"%' + search + '%"%';
564       var sql = new Sql();
565       sql.retrieve("select id from content where site_id = $0 and " +
566             "prototype = $1 and status <> $2 and (metadata like $3 or " +
567             "metadata like $4) order by created desc limit $5", 
568             this._id, "Story", Story.CLOSED, text, title, 25);
569       res.push();
570       var counter = 0;
571       sql.traverse(function() {
572          var story = Story.getById(this.id);
573          story.renderSkin("Story#result");
574          counter += 1;
575       });
576       res.message = ngettext("Found {0} result.", 
577             "Found {0} results.", counter);
578       res.data.body = res.pop();
579    }
580    
581    res.data.title = gettext('Search results for "{0}" in site "{1}"', 
582          search, this.title);
583    this.renderSkin("Site#page");
584    return;
585 }
586 
587 Site.prototype.subscribe_action = function() {
588    try {
589       var membership = new Membership(session.user, Membership.SUBSCRIBER);
590       this.members.add(membership);
591       res.message = gettext('Successfully subscribed to site {0}.', 
592             this.title);
593    } catch (ex) {
594       app.log(ex);
595       res.message = ex.toString();
596    }
597    res.redirect(this.href());
598    return;
599 }
600 
601 Site.prototype.unsubscribe_action = function() {
602    if (req.postParams.proceed) {
603       try {
604          Membership.remove(Membership.getByName(session.user.name));
605          res.message = gettext("Successfully unsubscribed from site {0}.",
606                this.title);
607          res.redirect(User.getLocation() || this.href());
608       } catch (ex) {
609          app.log(ex)
610          res.message = ex.toString();
611       }
612    }
613 
614    User.setLocation();
615    res.data.title = gettext("Remove subscription to {0}", this.title);
616    res.data.body = this.renderSkinAsString("$HopObject#confirm", {
617       text: gettext('You are about to unsubscribe from site {0}.', this.title)
618    });
619    this.renderSkin("Site#page");
620    return;
621 }
622 
623 Site.prototype.export_action = function() {
624    var fname = this.name + "-export.zip";
625    var zip = new helma.File(this.getStaticFile(fname));
626    if (req.postParams.submit === "export") {
627       if (Exporter.add(this)) {
628          res.message = "Site is queued for export";
629          zip.remove();
630       } else {
631          res.message = "Site is already being exported";
632       }
633       res.redirect(this.href(req.action));
634    }
635    var param = {
636       fileName: zip.getName(),
637       fileUrl: zip.exists() ? this.getStaticUrl(zip.getName()) : null,
638       fileDate: new Date(zip.lastModified())
639    }
640    res.data.body = this.renderSkinAsString("$Site#export", param);
641    this.renderSkin("Site#page");
642    return;
643 }
644 
645 Site.prototype.robots_txt_ction = function() {
646    res.contentType = "text/plain";
647    this.renderSkin("Site#robots");
648    return;
649 }
650 
651 /**
652  * 
653  * @param {String} name
654  * @returns {HopObject}
655  */
656 Site.prototype.getMacroHandler = function(name) {
657    switch (name) {
658       case "archive":
659       case "files":
660       case "galleries":
661       case "images":
662       case "layout":
663       case "members":
664       case "polls":
665       case "stories":
666       case "tags":
667       return this[name];
668       default:
669       return null;
670    }
671 }
672 
673 /**
674  * 
675  */
676 Site.prototype.stories_macro = function() {
677    if (this.stories.featured.size() < 1) {
678       this.renderSkin("Site#welcome");
679       if (session.user) {
680          if (session.user === this.creator) {
681             session.user.renderSkin("$User#welcome");
682          }
683          if (this === root && User.require(User.PRIVILEGED)) {
684             this.admin.renderSkin("$Admin#welcome");
685          }
686       }
687    } else {
688       this.archive.renderSkin("Archive#main");
689    }
690    return;
691 }
692 
693 /**
694  * 
695  * @param {Object} param
696  */
697 Site.prototype.calendar_macro = function(param) {
698    if (this.archiveMode !== Site.PUBLIC) {
699       return;
700    }
701    var calendar = new jala.Date.Calendar(this.archive);
702    //calendar.setAccessNameFormat("yyyy/MM/dd");
703    calendar.setHrefFormat("/yyyy/MM/dd/");
704    calendar.setLocale(this.getLocale());
705    calendar.setTimeZone(this.getTimeZone());
706    calendar.render(this.archive.getDate());
707    return;
708 }
709 
710 /**
711  * 
712  * @param {Object} param
713  */
714 Site.prototype.age_macro = function(param) {
715    res.write(Math.floor((new Date() - this.created) / Date.ONEDAY));
716    return;
717 }
718 
719 /**
720  * 
721  */
722 Site.prototype.referrers_macro = function() {
723    var self = this;
724    var sql = new Sql();
725    sql.retrieve("select referrer, count(*) as requests from " +
726          "log where context_type = 'Site' and context_id = $0 and action = " +
727          "'main' and created > date_add(now(), interval -1 day) group " +
728          "by referrer order by requests desc, referrer asc", this._id);
729    sql.traverse(function() {
730       if (this.requests && this.referrer) {
731          this.text = encode(this.referrer.head(50));
732          this.referrer = encode(this.referrer);
733          self.renderSkin("$Site#referrer", this);
734       }
735    });
736    return;
737 }
738 
739 /**
740  * @returns {java.util.Locale}
741  */
742 Site.prototype.getLocale = function() {
743    var locale;
744    if (locale = this.cache.locale) {
745       return locale;
746    } else if (this.locale) {
747       var parts = this.locale.split("_");
748       locale = new java.util.Locale(parts[0] || String.EMPTY, 
749             parts[1] || String.EMPTY, parts.splice(2).join("_"));
750    } else {
751       locale = java.util.Locale.getDefault();
752    }
753    return this.cache.locale = locale;
754 }
755 
756 /**
757  * @returns {java.util.TimeZone}
758  */
759 Site.prototype.getTimeZone = function() {
760    var timeZone;
761    if (timeZone = this.cache.timeZone) {
762       return timeZone;
763    }
764    if (this.timeZone) {
765        timeZone = java.util.TimeZone.getTimeZone(this.timeZone);
766    } else {
767        timeZone = java.util.TimeZone.getDefault();
768    }
769    this.cache.timezone = timeZone;
770    return timeZone;
771 }
772 
773 /**
774  * 
775  * @param {String} href
776  */
777 Site.prototype.processHref = function(href) {
778    var vhost = getProperty("vhost." + this.name, 
779          app.properties.defaultHost + "/" + this.name);
780    return vhost + href;
781 }
782 
783 /**
784  * 
785  * @param {String} type
786  * @param {String} group
787  * @returns {Tag[]}
788  */
789 Site.prototype.getTags = function(type, group) {
790    var handler;
791    type = type.toLowerCase();
792    switch (type) {
793       case "story":
794       case "tags":
795       handler = this.stories;
796       type = "tags";
797       break;
798       case "image":
799       case "galleries":
800       handler = this.images;
801       type = "galleries";
802       break;
803    }
804    switch (group) {
805       case Tags.ALL:
806       return handler[type];     
807       case Tags.OTHER:
808       case Tags.ALPHABETICAL:
809       return handler[group + type.titleize()];
810       default:
811       return handler["alphabetical" + type.titleize()].get(group);
812    }
813    return null;
814 }
815 
816 /**
817  * 
818  * @param {String} tail
819  * @returns {helma.File}
820  */
821 Site.prototype.getStaticFile = function(tail) {
822    res.push();
823    res.write(app.properties.staticPath);
824    res.write(this.name);
825    res.write("/");
826    tail && res.write(tail);
827    return new helma.File(res.pop());
828 }
829 
830 /**
831  * 
832  * @param {String} tail
833  * @returns {String}
834  */
835 Site.prototype.getStaticUrl = function(tail) {
836    res.push();
837    res.write(app.properties.staticUrl);
838    res.write(this.name);
839    res.write("/");
840    tail && res.write(tail);
841    return encodeURI(res.pop());
842 }
843 
844 /**
845  * 
846  * @param {Object} ref
847  */
848 Site.prototype.callback = function(ref) {
849     if (this.callbackMode === Site.ENABLED && this.callbackUrl) {
850       app.data.callbacks.push({
851          site: this._id,
852          handler: ref.constructor,
853          id: ref._id
854       });
855    }
856    return;
857 }
858 
859 /**
860  * 
861  * @param {String} name
862  * @returns {String[]}
863  */
864 Site.prototype.getAdminHeader = function(name) {
865    switch (name) {
866       case "tags":
867       case "galleries":
868       return ["#", "Name", "Items"];
869    }
870    return [];
871 }
872