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