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