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