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 Layout prototype
 28  */
 29 
 30 markgettext("Layout");
 31 markgettext("layout");
 32 
 33 /** @constant */
 34 Layout.VALUES = [
 35    "background color",
 36    "link color",
 37    "active link color",
 38    "visited link color",
 39    "big font",
 40    "big font size",
 41    "big font color",
 42    "base font",
 43    "base font size",
 44    "base font color",
 45    "small font",
 46    "small font size",
 47    "small font color"
 48 ];
 49 
 50 /**
 51  * @param {Site} site
 52  * @param {User} user
 53  * @returns {Layout}
 54  */
 55 Layout.add = function(site, user) {
 56    HopObject.confirmConstructor(Layout);
 57    var layout = new Layout;
 58    layout.site = site;
 59    layout.creator = user || session.user;
 60    layout.created = new Date;
 61    layout.mode = Layout.DEFAULT;
 62    layout.reset();
 63    layout.touch();
 64    site.layout = layout;
 65    return layout
 66 }
 67 
 68 /**
 69  * 
 70  * @param {Layout} layout
 71  * @param {Boolean} includeSelf
 72  */
 73 Layout.remove = function(options) {
 74    if (this.constructor === Layout) {
 75       // Backup current layout in temporary directory if possible
 76       var dir = this.getFile();
 77       // FIXME: Evaluate if using res.skinpath is necessary; dir could be fully sufficient.
 78       if (res.skinpath && res.skinpath[0] && res.skinpath[0].equals(dir) &&
 79                dir.exists() && dir.list().length > 0) {
 80          var zip = this.getArchive(res.skinpath);
 81          var file = java.io.File.createTempFile(this.site.name + "-layout-", ".zip");
 82          zip.save(file);
 83       }
 84       HopObject.remove.call(this.skins);
 85       HopObject.remove.call(this.images);
 86       this.getFile().removeDirectory();
 87       // The “force” flag is set e.g. when a whole site is removed
 88       if (options && options.force) {
 89          this.deleteMetadata();
 90          this.remove();
 91       }
 92    }
 93    return;
 94 }
 95 
 96 /**
 97  * @function
 98  * @param {Boolean} [value]
 99  * @returns {Boolean}
100  */
101 Layout.sandbox = function(value) {
102    var cookie = User.COOKIE + 'LayoutSandbox';
103    if (typeof value === 'undefined') {
104       return req.cookies[cookie] === 'true';
105    } 
106    if (value === true) {
107       res.setCookie(cookie, true);
108    } else if (value === false) {
109       res.unsetCookie(cookie);
110    }
111    return value;
112 }
113 
114 /** 
115  * @function
116  * @returns {String[]}
117  * @see defineConstants
118  */
119 Layout.getModes = defineConstants(Layout, "default", "shared");
120 
121 this.handleMetadata("origin");
122 this.handleMetadata("originator");
123 this.handleMetadata("originated");
124 
125 /**
126  * @name Layout
127  * @constructor
128  * @property {Date} created
129  * @property {User} creator
130  * @property {Images} images
131  * @property {Metadata} metadata
132  * @property {String} mode
133  * @property {Date} modified
134  * @property {User} modifier
135  * @property {String} origin
136  * @property {String} originator
137  * @property {Date} originated
138  * @property {Site} site
139  * @property {Skins} skins
140  * @extends HopObject
141  */
142 Layout.prototype.constructor = function() {
143    HopObject.confirmConstructor.call(this);
144    return this;
145 }
146 
147 /**
148  * 
149  * @param {String} action
150  * @returns {Boolean}
151  */
152 Layout.prototype.getPermission = function(action) {
153    switch (action) {
154       case ".":
155       case "main":
156       case "export":
157       case "images":
158       case "import":
159       case "reset":
160       case "skins":
161       case "sandbox":
162       return res.handlers.site.getPermission("main") &&
163             Membership.require(Membership.OWNER) ||
164             User.require(User.PRIVILEGED);
165    }
166    return false;
167 }
168 
169 // FIXME: The Layout.href method is overwritten to guarantee that
170 // URLs won't contain the layout ID instead of "layout"
171 /**
172  * 
173  * @param {String} action
174  * @returns {String}
175  */
176 Layout.prototype.href = function(action) {
177    res.push();
178    res.write(res.handlers.site.href());
179    res.write("layout/");
180    action && res.write(action);
181    return res.pop();
182 }
183 
184 Layout.prototype.main_action = function() {
185    if (req.postParams.save) {
186       try {
187          this.update(req.postParams);
188          res.message = gettext("Successfully updated the layout.");
189          res.redirect(this.href());
190       } catch (ex) {
191          res.message = ex;
192          app.log(ex);
193       }
194    }
195    res.data.title = gettext("Layout");
196    res.data.body = this.renderSkinAsString("$Layout#main");
197    res.handlers.site.renderSkin("Site#page");
198    return;
199 }
200 
201 /**
202  * 
203  * @param {String} name
204  * @returns {Object}
205  */
206 Layout.prototype.getFormOptions = function(name) {
207    switch (name) {
208       case "mode":
209       return Layout.getModes();
210       case "parent":
211       return this.getParentOptions();
212    }
213 }
214 
215 /**
216  * 
217  * @param {Object} data
218  */
219 Layout.prototype.update = function(data) {
220    var skin = this.skins.getSkin("Site", "values");
221    if (!skin) {
222       Skin.add("Site", "values", this);
223    }
224    res.push();
225    for (var key in data) {
226       if (key.startsWith("value_")) {
227          var value = data[key];
228          key = key.substr(6);
229          res.write("<% value ");
230          res.write(quote(key));
231          res.write(" ");
232          res.write(quote(value));
233          res.write(" %>\n");
234       }
235    }
236    res.write("\n");
237    skin.setSource(res.pop());
238    Layout.sandbox(!!data.sandbox);   
239    this.description = data.description;
240    this.mode = data.mode;
241    this.touch();
242    return;
243 }
244 
245 Layout.prototype.reset_action = function() {
246    if (req.data.proceed) {
247       try {
248          Layout.remove.call(this);
249          this.reset();
250          res.message = gettext("{0} was successfully reset.", gettext("Layout"));
251          res.redirect(this.href());
252       } catch(ex) {
253          res.message = ex;
254          app.log(ex);
255       }
256    }
257 
258    res.data.action = this.href(req.action);
259    res.data.title = gettext("Confirm Reset");
260    res.data.body = this.renderSkinAsString("$HopObject#confirm", {
261       text: this.getConfirmText()
262    });
263    res.handlers.site.renderSkin("Site#page");
264 }
265 
266 Layout.prototype.export_action = function() {
267    res.contentType = "application/zip";
268    var zip = this.getArchive(res.skinpath);
269    res.setHeader("Content-Disposition",
270          "attachment; filename=" + this.site.name + "-layout.zip");
271    res.writeBinary(zip.getData());
272    return;
273 }
274 
275 Layout.prototype.import_action = function() {
276    var self = this;
277    var data = req.postParams;
278    if (data.submit) {
279       try {
280          if (!data.upload || data.upload.contentLength === 0) {
281             throw Error(gettext("Please upload a zipped layout archive"));
282          }
283          // Extract zipped layout to temporary directory
284          var dir = this.site.getStaticFile();
285          var temp = new helma.File(dir, "import.temp");
286          var fname = data.upload.writeToFile(dir);
287          var zip = new helma.File(dir, fname);
288          (new helma.Zip(zip)).extractAll(temp);
289          zip.remove();
290          var data = Xml.read(new helma.File(temp, "data.xml"));
291          if (!data.version) {
292             throw Error(gettext("Sorry, this layout is not compatible with Antville."));
293          }
294          // Remove current layout and replace it with imported one
295          Layout.remove.call(this);
296          temp.renameTo(this.getFile());
297          this.origin = data.origin;
298          this.originator = data.originator;
299          this.originated = data.originated;
300          data.images.forEach(function() {
301             Image.add(this);
302          });
303          this.touch();
304          res.message = gettext("The layout was successfully imported.");
305       } catch (ex) {
306          res.message = ex;
307          app.log(ex);
308          temp && temp.removeDirectory();
309          res.redirect(this.href(req.action));
310       }
311       res.redirect(this.href());
312       return;
313    }
314    res.data.title = gettext("Import Layout");
315    res.data.body = this.renderSkinAsString("$Layout#import");
316    res.handlers.site.renderSkin("Site#page");
317    return;
318 }
319 
320 /**
321  * 
322  * @param {String} name
323  * @param {String} fallback
324  * @returns {Image}
325  */
326 Layout.prototype.getImage = function(name, fallback) {
327    var layout = this;
328    while (layout) {
329       layout.images.prefetchChildren();
330       if (layout.images.get(name)) {
331          return layout.images.get(name);
332       }
333       if (fallback && layout.images.get(fallback)) {
334          return layout.images.get(fallback);
335       }
336       layout = layout.parent;
337    }
338    return null;
339 }
340 
341 /**
342  * 
343  * @param {String} name
344  * @returns {helma.File}
345  */
346 Layout.prototype.getFile = function(name) {
347    name || (name = String.EMPTY);
348    return this.site.getStaticFile("layout/" + name);
349 }
350 
351 /**
352  * @returns {String[]}
353  */
354 Layout.prototype.getSkinPath = function() {
355    if (!this.site) {
356       return null;
357    }
358    var skinPath = [this.getFile().toString()];
359    return skinPath;
360 }
361 
362 /**
363  * 
364  */
365 Layout.prototype.reset = function() {
366    var skinFiles = app.getSkinfilesInPath([app.dir]);
367    var content, dir, file;
368    for (var name in skinFiles) {
369       if (content = skinFiles[name][name]) {
370          dir = this.getFile(name);
371          file = new helma.File(dir, name + ".skin");
372          dir.makeDirectory();
373          file.open();
374          file.write(content);
375          file.close();
376       }
377    }
378 
379    // FIXME: Reset the Site skin of root separately
380    content = skinFiles.Root.Site;
381    file = new helma.File(this.getFile("Root"), "Site.skin");
382    dir.makeDirectory();
383    file.open();
384    file.write(content);
385    file.close()
386 
387    this.touch();
388    return;
389 }
390 
391 /**
392  * 
393  * @param {String} skinPath
394  * @returns {helma.Zip}
395  */
396 Layout.prototype.getArchive = function(skinPath) {
397    var zip = new helma.Zip();
398    var skinFiles = app.getSkinfilesInPath(skinPath);
399    for (var name in skinFiles) {
400       if (skinFiles[name][name]) {
401          var file = new helma.File(this.getFile(name), name + ".skin");
402          if (file.exists()) {
403             zip.add(file, name);
404          }
405       }
406    }
407 
408    // FIXME: Add the Site skin of root separately
409    file = new helma.File(this.getFile("Root"), "Site.skin");
410    file.exists() && zip.add(file, "Root");
411 
412    var data = new HopObject;
413    data.images = new HopObject;
414    this.images.forEach(function() {
415       zip.add(this.getFile());
416       try {
417          zip.add(this.getThumbnailFile());
418       } catch (ex) {
419          /* Most likely the thumbnail file is identical to the image */ 
420       }
421       var image = new HopObject;
422       for each (var key in Image.KEYS) {
423          image[key] = this[key];
424          data.images.add(image);
425       }
426    });
427       
428    data.version = Root.VERSION.toString();
429    data.origin = this.origin || this.site.href();
430    data.originator = this.originator || session.user.name;
431    data.originated = this.originated || new Date;
432    
433    // FIXME: XML encoder is losing all mixed-case properties :(
434    var xml = new java.lang.String(Xml.writeToString(data));
435    zip.addData(xml.getBytes("UTF-8"), "data.xml");
436    zip.close();
437    return zip;
438 }
439 
440 
441 /**
442  * @returns {String}
443  */
444 Layout.prototype.getConfirmText = function() {
445    return gettext("You are about to reset the layout of site {0}.", 
446          this.site.name);
447 }
448 /**
449  * 
450  * @param {String} name
451  * @returns {HopObject}
452  */
453 Layout.prototype.getMacroHandler = function(name) {
454    switch (name) {
455       case "skins":
456       return this[name];
457       
458       default:
459       return null;
460    }
461 }
462 
463 /**
464  * 
465  * @param {Object} param
466  * @param {String} name
467  * @param {String} mode
468  */
469 Layout.prototype.image_macro = function(param, name, mode) {
470    name || (name = param.name);
471    if (!name) {
472       return;
473    }
474 
475    var image = this.getImage(name, param.fallback);
476    if (!image) {
477       return;
478    }
479 
480    mode || (mode = param.as);
481    var action = param.linkto;
482    delete(param.name);
483    delete(param.as);
484    delete(param.linkto);
485 
486    switch (mode) {
487       case "url" :
488       return res.write(image.getUrl());
489       case "thumbnail" :
490       action || (action = image.getUrl());
491       return image.thumbnail_macro(param);
492    }
493    image.render_macro(param);
494    return;
495 }
496 
497 /**
498  * 
499  */
500 Layout.prototype.values_macro = function() {
501    var values = [];
502    for (var key in res.meta.values) {
503       values.push({key: key, value: res.meta.values[key]});
504    }
505    values.sort(new String.Sorter("key"));
506    for each (var pair in values) {
507       this.renderSkin("$Layout#value", {
508          key: pair.key.capitalize(), 
509          value: pair.value
510       });
511    }
512    return;
513 }
514 
515 /**
516  *
517  */
518 Layout.prototype.sandbox_macro = function() {
519    res.write(Layout.sandbox());
520    return;
521 }
522 
523