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