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