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