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