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