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