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