      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