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