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