      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 extensions of Helma’s built-in
     28  * HopObject prototype.
     29  */
     30
     31 /**
     32  *
     33  * @param {HopObject} collection
     34  * @param {Object} options Optional flags, e.g. to force or prevent any
     35  * conditional checks of individual prototype’s remove() methods
     36  */
     37 HopObject.remove = function(options) {
     38    var item;
     39    while (this.size() > 0) {
     40       item = this.get(0);
     41       if (item.constructor.remove) {
     42          item.constructor.remove.call(item, options);
     43       } else if (!options) {
     44          item.remove();
     45       } else {
     46          throw Error("Missing static " + item.constructor.name +
     47                ".remove() method");
     48       }
     49    }
     50    return;
     51 }
     52
     53 /**
     54  *
     55  * @param {String} name
     56  * @param {HopObject} collection
     57  */
     58 HopObject.getFromPath = function(name, collection) {
     59    if (name) {
     60       var site;
     61       if (name.contains("/")) {
     62          var parts = name.split("/");
     63          site = root.get(parts[0]);
     64          name = parts[1];
     65       } else {
     66          site = res.handlers.site;
     67       }
     68       if (site && site.getPermission("main")) {
     69          return site[collection].get(name);
     70       }
     71    }
     72    return null;
     73 }
     74
     75 /**
     76  * Helma’s built-in HopObject with Antville’s extensions.
     77  * @name HopObject
     78  * @constructor
     79  */
     80
     81 /**
     82  *
     83  * @param {Object} values
     84  */
     85 HopObject.prototype.map = function(values) {
     86    for (var i in values) {
     87       this[i] = values[i];
     88    }
     89    return;
     90 }
     91
     92 /**
     93  *
     94  */
     95 HopObject.prototype.onRequest = function() {
     96    // Checking if we are on the correct host to prevent at least some XSS issues
     97    if (req.action !== "notfound" && req.action !== "error" &&
     98          this.href().startsWith("http://") &&
     99          !this.href().toLowerCase().startsWith(req.servletRequest.scheme +
    100          "://" + req.servletRequest.serverName.toLowerCase())) {
    101       res.redirect(this.href(req.action === "main" ? String.EMPTY : req.action));
    102    }
    103
    104    User.autoLogin();
    105    res.handlers.membership = User.getMembership();
    106
    107    if (User.getCurrentStatus() === User.BLOCKED) {
    108       session.data.status = 403;
    109       session.data.error = gettext("Your account has been blocked.") + String.SPACE +
    110             gettext("Please contact an administrator for further information.");
    111       User.logout();
    112       res.redirect(root.href("error"));
    113    }
    114
    115    if (res.handlers.site.status === Site.BLOCKED &&
    116          !User.require(User.PRIVILEGED)) {
    117       session.data.status = 403;
    118       session.data.error = gettext("The site you requested has been blocked.") +
    119             String.SPACE + gettext("Please contact an administrator for further information.");
    120       res.redirect(root.href("error"));
    121    }
    122
    123    res.handlers.layout = res.handlers.site.layout || new Layout;
    124    res.skinpath = res.handlers.layout.getSkinPath();
    125
    126    if (!this.getPermission(req.action)) {
    127       if (!session.user) {
    128          User.setLocation(root.href() + req.path);
    129          res.message = gettext("Please login first.");
    130          res.redirect(res.handlers.site.members.href("login"));
    131       }
    132       User.getLocation();
    133       res.status = 401;
    134       res.data.title = gettext("{0} 401 Error", root.title);
    135       res.data.body = root.renderSkinAsString("$Root#error", {error:
    136             gettext("You are not allowed to access this part of the site.")});
    137       res.handlers.site.renderSkin("Site#page");
    138       session.data.error = null;
    139       res.stop();
    140    }
    141
    142    res.meta.values = {};
    143    res.handlers.site.renderSkinAsString("Site#values");
    144    return;
    145 }
    146
    147 /**
    148  * @returns Boolean
    149  */
    150 HopObject.prototype.getPermission = function() {
    151    return true;
    152 }
    153
    154 // Marking some prototype names used in res.message of HopObject.delete_action()
    155 markgettext("Comment");
    156 markgettext("File");
    157 markgettext("Image");
    158 markgettext("Membership");
    159 markgettext("Poll");
    160 markgettext("Story");
    161
    162 HopObject.prototype.delete_action = function() {
    163    if (req.postParams.proceed) {
    164       try {
    165          var parent = this._parent;
    166          var url = this.constructor.remove.call(this, req.postParams) ||
    167                parent.href();
    168          res.message = gettext("{0} was successfully deleted.", gettext(this._prototype));
    169          res.redirect(User.getLocation() || url);
    170       } catch(ex) {
    171          res.message = ex;
    172          app.log(ex);
    173       }
    174    }
    175
    176    res.data.action = this.href(req.action);
    177    res.data.title = gettext("Confirm Deletion");
    178    res.data.body = this.renderSkinAsString("$HopObject#confirm", {
    179       text: this.getConfirmText()
    180    });
    181    res.handlers.site.renderSkin("Site#page");
    182    return;
    183 }
    184
    185 /**
    186  * @returns {Object}
    187  */
    188 HopObject.prototype.touch = function() {
    189    return this.map({
    190       modified: new Date,
    191       modifier: session.user
    192    });
    193 }
    194
    195 /**
    196  *
    197  */
    198 HopObject.prototype.log = function() {
    199    var entry = new LogEntry(this, "main");
    200    app.data.entries.push(entry);
    201    return;
    202 }
    203
    204 /**
    205  *
    206  * @param {String} action
    207  */
    208 HopObject.prototype.notify = function(action) {
    209    var self = this;
    210    var site = res.handlers.site;
    211
    212    var getPermission = function(scope, mode, status) {
    213       if (scope === Admin.NONE || mode === Site.NOBODY ||
    214             status === Site.BLOCKED) {
    215          return false;
    216       }
    217       var scopes = [Admin.REGULAR, Admin.TRUSTED];
    218       if (scopes.indexOf(status) < scopes.indexOf(scope)) {
    219          return false;
    220       }
    221       if (!Membership.require(mode)) {
    222          return false;
    223       }
    224       return true;
    225    }
    226
    227    // Helper method for debugging
    228    var renderMatrix = function() {
    229       var buf = ['<table border=1 cellspacing=0>'];
    230       for each (var scope in Admin.getNotificationScopes()) {
    231          for each (var mode in Site.getNotificationModes()) {
    232             for each (var status in Site.getStatus()) {
    233                var perm = getPermission(scope.value, mode.value, status.value);
    234                buf.push('<tr style="');
    235                perm && buf.push('color: blue;');
    236                if (scope.value === root.notificationScope && mode.value ===
    237                      site.notificationMode && status.value === site.status) {
    238                   buf.push(' background-color: yellow;');
    239                }
    240                buf.push('">');
    241                buf.push('<td>', scope.value, '</td>');
    242                buf.push('<td>', status.value, '</td>');
    243                buf.push('<td>', mode.value, '</td>');
    244                buf.push('<td>', perm, '</td>');
    245                buf.push('</tr>');
    246             }
    247          }
    248       }
    249       buf.push('</table>');
    250       res.write(buf.join(""));
    251       return;
    252    }
    253
    254    switch (action) {
    255       case "comment":
    256       action = "create"; break;
    257    }
    258
    259    var currentMembership = res.handlers.membership;
    260    site.members.forEach(function() {
    261       var membership = res.handlers.membership = this;
    262       if (getPermission(root.notificationScope, site.notificationMode, site.status)) {
    263          sendMail(membership.creator.email, gettext("[{0}] Notification of site changes",
    264                root.title), self.renderSkinAsString("$HopObject#notify_" + action));
    265       }
    266    });
    267    res.handlers.membership = currentMembership;
    268    return;
    269 }
    270
    271 /**
    272  * @returns {Tag[]}
    273  */
    274 HopObject.prototype.getTags = function() {
    275    var tags = [];
    276    if (typeof this.tags === "object") {
    277       this.tags.list().forEach(function(item) {
    278          item.tag && tags.push(item.tag.name);
    279       });
    280    }
    281    return tags;
    282 }
    283
    284 /**
    285  *
    286  * @param {Tag[]|String} tags
    287  */
    288 HopObject.prototype.setTags = function(tags) {
    289    if (typeof this.tags !== "object") {
    290       return String.EMPTY;
    291    }
    292
    293    if (!tags) {
    294       tags = [];
    295    } else if (tags.constructor === String) {
    296       tags = tags.split(/\s*,\s*/);
    297    }
    298
    299    var diff = {};
    300    var tag;
    301    for (var i in tags) {
    302       // Trim and remove troublesome characters  (like ../.. etc.)
    303       // We call getAccessName with a virgin HopObject to allow most names
    304       tag = tags[i] = this.getAccessName.call(new HopObject, File.getName(tags[i]));
    305       if (tag && diff[tag] == null) {
    306          diff[tag] = 1;
    307       }
    308    }
    309    this.tags.forEach(function() {
    310       if (!this.tag) {
    311          return;
    312       }
    313       diff[this.tag.name] = (tags.indexOf(this.tag.name) < 0) ? this : 0;
    314    });
    315
    316    for (var tag in diff) {
    317       switch (diff[tag]) {
    318          case 0:
    319          // Do nothing (tag already exists)
    320          break;
    321          case 1:
    322          // Add tag to story
    323          this.addTag(tag);
    324          break;
    325          default:
    326          // Remove tag
    327          this.removeTag(diff[tag]);
    328       }
    329    }
    330    return;
    331 }
    332
    333 /**
    334  *
    335  * @param {String} name
    336  */
    337 HopObject.prototype.addTag = function(name) {
    338    this.tags.add(new TagHub(name, this, session.user));
    339    return;
    340 }
    341
    342 /**
    343  *
    344  * @param {String} tag
    345  */
    346 HopObject.prototype.removeTag = function(tag) {
    347    var parent = tag._parent;
    348    if (parent.size() === 1) {
    349       parent.remove();
    350    }
    351    tag.remove();
    352    return;
    353 }
    354
    355 /**
    356  *
    357  * @param {Object} param
    358  * @param {String} name
    359  */
    360 HopObject.prototype.skin_macro = function(param, name) {
    361    if (!name) {
    362       return;
    363    }
    364    if (name.contains("#")) {
    365       this.renderSkin(name);
    366    } else {
    367       var prototype = this._prototype || "Global";
    368       this.renderSkin(prototype + "#" + name);
    369    }
    370    return;
    371 }
    372
    373 /**
    374  *
    375  * @param {Object} param
    376  * @param {String} name
    377  */
    378 HopObject.prototype.input_macro = function(param, name) {
    379    param.name = name;
    380    param.id = name;
    381    param.value = this.getFormValue(name);
    382    return html.input(param);
    383 }
    384
    385 /**
    386  *
    387  * @param {Object} param
    388  * @param {String} name
    389  */
    390 HopObject.prototype.textarea_macro = function(param, name) {
    391    param.name = name;
    392    param.id = name;
    393    param.value = this.getFormValue(name);
    394    return html.textArea(param);
    395 }
    396
    397 /**
    398  *
    399  * @param {Object} param
    400  * @param {String} name
    401  */
    402 HopObject.prototype.select_macro = function(param, name) {
    403    param.name = name;
    404    param.id = name;
    405    var options = this.getFormOptions(name);
    406    if (options.length < 2) {
    407       param.disabled = "disabled";
    408    }
    409    return html.dropDown(param, options, this.getFormValue(name));
    410 }
    411
    412 /**
    413  *
    414  * @param {Object} param
    415  * @param {String} name
    416  */
    417 HopObject.prototype.checkbox_macro = function(param, name) {
    418    param.name = name;
    419    param.id = name;
    420    var options = this.getFormOptions(name);
    421    if (options.length < 2) {
    422       param.disabled = "disabled";
    423    }
    424    param.value = String((options[1] || options[0]).value);
    425    param.selectedValue = String(this.getFormValue(name));
    426    var label = param.label;
    427    delete param.label;
    428    html.checkBox(param);
    429    if (label) {
    430       html.element("label", label, {"for": name});
    431    }
    432    return;
    433 }
    434
    435 /**
    436  *
    437  * @param {Object} param
    438  * @param {String} name
    439  */
    440 HopObject.prototype.radiobutton_macro = function(param, name) {
    441    param.name = name;
    442    param.id = name;
    443    var options = this.getFormOptions(name);
    444    if (options.length < 2) {
    445       param.disabled = "disabled";
    446    }
    447    param.value = String(options[0].value);
    448    param.selectedValue = String(this.getFormValue(name));
    449    var label = param.label;
    450    delete param.label;
    451    html.radioButton(param);
    452    if (label) {
    453       html.element("label", label, {"for": name});
    454    }
    455    return;
    456 }
    457
    458 /**
    459  *
    460  * @param {Object} param
    461  * @param {String} name
    462  */
    463 HopObject.prototype.upload_macro = function(param, name) {
    464    param.name = name;
    465    param.id = name;
    466    param.value = this.getFormValue(name);
    467    renderSkin("$Global#upload", param);
    468    return;
    469 }
    470
    471 /**
    472  *
    473  * @param {Object} param
    474  * @param {HopObject} [handler]
    475  */
    476 HopObject.prototype.macro_macro = function(param, handler) {
    477    var ctor = this.constructor;
    478    if ([Story, Image, File, Poll].indexOf(ctor) > -1) {
    479       res.encode("<% ");
    480       res.write(handler || ctor.name.toLowerCase());
    481       res.write(String.SPACE);
    482       res.write(quote(this.name || this._id));
    483       res.encode(" %>");
    484    }
    485    return;
    486 }
    487
    488 /**
    489  *
    490  */
    491 HopObject.prototype.kind_macro = function() {
    492    var type = this.constructor.name.toLowerCase();
    493    switch (type) {
    494       default:
    495       res.write(gettext(type));
    496       break;
    497    }
    498    return;
    499 }
    500
    501 /**
    502  *
    503  * @param {String} name
    504  * @returns {Number|String}
    505  */
    506 HopObject.prototype.getFormValue = function(name) {
    507    if (req.isPost()) {
    508       return req.postParams[name];
    509    } else {
    510       var value = this[name] || req.queryParams[name] || String.EMPTY;
    511       return value instanceof HopObject ? value._id : value;
    512    }
    513 }
    514
    515 /**
    516  * @returns {Object[]}
    517  */
    518 HopObject.prototype.getFormOptions = function() {
    519    return [{value: true, display: "enabled"}];
    520 }
    521
    522 /**
    523  * @returns {HopObject}
    524  * @param {Object} param
    525  * @param {String} property
    526  */
    527 HopObject.prototype.self_macro = function(param, property) {
    528    return property ? this[property] : this;
    529 }
    530
    531 /**
    532  *
    533  */
    534 HopObject.prototype.type_macro = function() {
    535    return res.write(this.constructor.name);
    536 }
    537
    538 /**
    539  *
    540  * @param {Object} param
    541  * @param {String} url
    542  * @param {String} text
    543  */
    544 HopObject.prototype.link_macro = function(param, url, text) {
    545    if (url && text) {
    546       var action = url.split(/#|\?/)[0];
    547       if (this.getPermission(action)) {
    548          renderLink.call(global, param, url, text, this);
    549       }
    550    } else {
    551       res.write("[Insufficient link parameters]");
    552    }
    553    return;
    554 }
    555
    556 /**
    557  *
    558  * @param {Object} param
    559  * @param {String} format
    560  */
    561 HopObject.prototype.created_macro = function(param, format) {
    562    if (this.isPersistent()) {
    563       format || (format = param.format);
    564       res.write(formatDate(this.created, format));
    565    }
    566    return;
    567 }
    568
    569 /**
    570  *
    571  * @param {Object} param
    572  * @param {String} format
    573  */
    574 HopObject.prototype.modified_macro = function(param, format) {
    575    if (this.isPersistent()) {
    576       format || (format = param.format);
    577       res.write(formatDate(this.modified, format));
    578    }
    579    return;
    580 }
    581
    582 /**
    583  *
    584  * @param {Object} param
    585  * @param {String} mode
    586  */
    587 HopObject.prototype.creator_macro = function(param, mode) {
    588    if (!this.creator || this.isTransient()) {
    589       return;
    590    }
    591    mode || (mode = param.as);
    592    if (mode === "link" && this.creator.url) {
    593       html.link({href: this.creator.url}, this.creator.name);
    594    } else if (mode === "url") {
    595       res.write(this.creator.url);
    596    } else {
    597       res.write(this.creator.name);
    598    } return;
    599 }
    600
    601 /**
    602  *
    603  * @param {Object} param
    604  * @param {String} mode
    605  */
    606 HopObject.prototype.modifier_macro = function(param, mode) {
    607    if (!this.modifier || this.isTransient()) {
    608       return;
    609    }
    610    mode || (mode = param.as);
    611    if (mode === "link" && this.modifier.url) {
    612       html.link({href: this.modifier.url}, this.modifier.name);
    613    } else if (mode === "url") {
    614       res.write(this.modifier.url);
    615    } else {
    616       res.write(this.modifier.name);
    617    }
    618    return;
    619 }
    620
    621 /**
    622  * @returns {String}
    623  */
    624 HopObject.prototype.getTitle = function() {
    625    return this.title || gettext(this.__name__.capitalize());
    626 }
    627
    628 /**
    629  * @returns {String}
    630  */
    631 HopObject.prototype.toString = function() {
    632    return this.constructor.name + " #" + this._id;
    633 }
    634
    635 /**
    636  *
    637  * @param {String} text
    638  * @param {Object} param
    639  * @param {String} action
    640  * @returns {String}
    641  */
    642 HopObject.prototype.link_filter = function(text, param, action) {
    643    action || (action = ".");
    644    res.push();
    645    renderLink(param, action, text, this);
    646    return res.pop();
    647 }
    648