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