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