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