      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