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