      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