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