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 /** @constant */ 30 Layout.VALUES = [ 31 "background color", 32 "link color", 33 "active link color", 34 "visited link color", 35 "big font", 36 "big font size", 37 "big font color", 38 "base font", 39 "base font size", 40 "base font color", 41 "small font", 42 "small font size", 43 "small font color" 44 ]; 45 46 /** 47 * 48 * @param {Layout} layout 49 */ 50 Layout.remove = function(layout) { 51 layout || (layout = this); 52 if (layout.constructor === Layout) { 53 Skins.remove.call(layout.skins); 54 Images.remove.call(layout.images); 55 layout.getFile().removeDirectory(); 56 } 57 layout.remove(); 58 return; 59 } 60 61 /** 62 * @function 63 * @returns {String[]} 64 * @see defineConstants 65 */ 66 Layout.getModes = defineConstants(Layout, "default", "shared"); 67 68 this.handleMetadata("title"); 69 this.handleMetadata("description"); 70 this.handleMetadata("origin"); 71 this.handleMetadata("originator"); 72 this.handleMetadata("originated"); 73 74 /** 75 * @name Layout 76 * @constructor 77 * @param {Site} site 78 * @property {Date} created 79 * @property {User} creator 80 * @property {String} description 81 * @property {Images} images 82 * @property {Metadata} metadata 83 * @property {String} mode 84 * @property {Date} modified 85 * @property {User} modifier 86 * @property {String} origin 87 * @property {String} originator 88 * @property {Date} originated 89 * @property {Site} site 90 * @property {Skins} skins 91 * @property {String} title 92 * @extends HopObject 93 */ 94 Layout.prototype.constructor = function(site) { 95 this.site = site; 96 this.creator = session.user; 97 this.created = new Date; 98 this.mode = Layout.DEFAULT; 99 this.touch(); 100 return this; 101 } 102 103 /** 104 * 105 * @param {String} action 106 * @returns {Boolean} 107 */ 108 Layout.prototype.getPermission = function(action) { 109 switch (action) { 110 case ".": 111 case "main": 112 case "export": 113 case "images": 114 case "import": 115 case "reset": 116 case "skins": 117 return res.handlers.site.getPermission("main") && 118 Membership.require(Membership.OWNER) || 119 User.require(User.PRIVILEGED); 120 } 121 return false; 122 } 123 124 // FIXME: The Layout.href method is overwritten to guarantee that 125 // URLs won't contain the layout ID instead of "layout" 126 /** 127 * 128 * @param {String} action 129 * @returns {String} 130 */ 131 Layout.prototype.href = function(action) { 132 res.push(); 133 res.write(res.handlers.site.href()); 134 res.write("layout/"); 135 action && res.write(action); 136 return res.pop(); 137 } 138 139 Layout.prototype.main_action = function() { 140 if (req.postParams.save) { 141 try { 142 this.update(req.postParams); 143 res.message = gettext("Successfully updated the layout {0}.", 144 this.title); 145 res.redirect(this.href()); 146 } catch (ex) { 147 res.message = ex; 148 app.log(ex); 149 } 150 } 151 res.data.title = gettext("Layout of {0}", res.handlers.site.title); 152 res.data.body = this.renderSkinAsString("$Layout#main"); 153 res.handlers.site.renderSkin("Site#page"); 154 return; 155 } 156 157 /** 158 * 159 * @param {String} name 160 * @returns {Object} 161 */ 162 Layout.prototype.getFormOptions = function(name) { 163 switch (name) { 164 case "mode": 165 return Layout.getModes(); 166 case "parent": 167 return this.getParentOptions(); 168 } 169 } 170 171 /** 172 * 173 * @param {Object} data 174 */ 175 Layout.prototype.update = function(data) { 176 var skin = this.skins.getSkin("Site", "values"); 177 if (!skin) { 178 skin = new Skin("Site", "values"); 179 this.skins.add(skin); 180 } 181 res.push(); 182 for (var key in data) { 183 if (key.startsWith("value_")) { 184 var value = data[key]; 185 key = key.substr(6); 186 res.write("<% value "); 187 res.write(quote(key)); 188 res.write(" "); 189 res.write(quote(value)); 190 res.write(" %>\n"); 191 } 192 } 193 res.write("\n"); 194 skin.setSource(res.pop()); 195 this.description = data.description; 196 this.mode= data.mode; 197 this.touch(); 198 return; 199 } 200 201 Layout.prototype.reset_action = function() { 202 if (req.data.proceed) { 203 try { 204 var site = this.site; 205 Layout.remove.call(this); 206 // FIXME: The layout is now removed from DB and a new one needs to be added 207 site.layout = new Layout(site); 208 var skinFiles = app.getSkinfilesInPath(res.skinpath); 209 var content, file; 210 for (var name in skinFiles) { 211 if (content = skinFiles[name][name]) { 212 var dir = this.getFile(name); 213 var file = new helma.File(dir, name + ".skin"); 214 dir.makeDirectory(); 215 file.open(); 216 file.write(content); 217 file.close(); 218 } 219 } 220 res.message = gettext("The layout was successfully reset."); 221 res.redirect(this.href()); 222 } catch(ex) { 223 res.message = ex; 224 app.log(ex); 225 } 226 } 227 228 res.data.action = this.href(req.action); 229 res.data.title = gettext("Confirm reset of {0}", this); 230 res.data.body = this.renderSkinAsString("$HopObject#confirm", { 231 text: gettext('You are about to reset {0}.', this) 232 }); 233 res.handlers.site.renderSkin("Site#page"); 234 } 235 236 Layout.prototype.export_action = function() { 237 var zip = this.getArchive(res.skinpath); 238 res.contentType = "application/zip"; 239 res.setHeader("Content-Disposition", 240 "attachment; filename=" + this.site.name + "-layout.zip"); 241 res.writeBinary(zip.getData()); 242 return; 243 } 244 245 Layout.prototype.import_action = function() { 246 var data = req.postParams; 247 if (data.submit) { 248 try { 249 if (!data.upload || data.upload.contentLength === 0) { 250 throw Error(gettext("Please upload a layout package.")); 251 } 252 // Create destination directory 253 var destination = this.getFile(); 254 destination.makeDirectory(); 255 // Extract imported layout to temporary directory 256 var dir = new helma.File(destination, ".."); 257 var temp = new helma.File(dir, "import.temp"); 258 var fname = data.upload.writeToFile(dir); 259 var zip = new helma.File(dir, fname); 260 (new helma.Zip(zip)).extractAll(temp); 261 zip.remove(); 262 var data = Xml.read(new helma.File(temp, "data.xml")); 263 if (!data.version || data.version !== Root.VERSION) { 264 throw Error("Incompatible layout version."); 265 } 266 // Backup the current layout if possible 267 if (destination.list().length > 0) { 268 var timestamp = (new Date).format("yyyyMMdd-HHmmss"); 269 var zip = this.getArchive(res.skinpath); 270 zip.save(this.getFile("../layout-" + timestamp + ".zip")); 271 } 272 // Remove old layout and replace it with new one 273 Layout.remove(this); 274 res.commit(); 275 destination.makeDirectory(); // FIXME: This is in fact necessary again (s. line 197) 276 temp.renameTo(destination); 277 // Update database with imported data 278 layout = this; 279 this.origin = data.origin; 280 this.originator = data.originator; 281 this.originated = data.originated; 282 data.images.forEach(function() { 283 layout.images.add(new Image(this)); 284 }); 285 } catch (ex) { 286 res.message = ex; 287 app.log(ex); 288 } 289 res.redirect(this.href()); 290 return; 291 } 292 res.data.title = gettext("Import layout"); 293 res.data.body = this.renderSkinAsString("$Layout#import"); 294 res.handlers.site.renderSkin("Site#page"); 295 return; 296 } 297 298 /** 299 * @returns {String} 300 */ 301 Layout.prototype.getTitle = function() { 302 return "Layout"; 303 } 304 305 /** 306 * 307 * @param {String} name 308 * @param {String} fallback 309 * @returns {Image} 310 */ 311 Layout.prototype.getImage = function(name, fallback) { 312 var layout = this; 313 while (layout) { 314 if (layout.images.get(name)) { 315 return layout.images.get(name); 316 } 317 if (fallback && layout.images.get(fallback)) { 318 return layout.images.get(fallback); 319 } 320 layout = layout.parent; 321 } 322 return null; 323 } 324 325 /** 326 * 327 * @param {String} name 328 * @returns {helma.File} 329 */ 330 Layout.prototype.getFile = function(name) { 331 name || (name = String.EMPTY); 332 return this.site.getStaticFile("layout/" + name); 333 } 334 335 Layout.prototype.getSkinPath = function() { 336 if (!this.site) { 337 return null; 338 } 339 var skinPath = [this.getFile().toString()]; 340 return skinPath; 341 } 342 343 /** 344 * 345 * @param {String} skinPath 346 * @returns {helma.Zip} 347 */ 348 Layout.prototype.getArchive = function(skinPath) { 349 var zip = new helma.Zip(); 350 var skinFiles = app.getSkinfilesInPath(skinPath); 351 for (var name in skinFiles) { 352 if (skinFiles[name][name]) { 353 var file = new helma.File(this.getFile(name), name + ".skin"); 354 if (file.exists()) { 355 zip.add(file, name); 356 } 357 } 358 } 359 360 var data = new HopObject; 361 data.images = new HopObject; 362 this.images.forEach(function() { 363 zip.add(this.getFile()); 364 try { 365 zip.add(this.getThumbnailFile()); 366 } catch (ex) { 367 /* Most likely the thumbnail file is identical to the image */ 368 } 369 var image = new HopObject; 370 for each (var key in Image.KEYS) { 371 image[key] = this[key]; 372 data.images.add(image); 373 } 374 }); 375 376 data.version = Root.VERSION; 377 data.origin = this.origin || this.site.href(); 378 data.originator = this.originator || session.user.name; 379 data.originated = this.originated || new Date; 380 381 // FIXME: XML encoder is losing all mixed-case properties :( 382 var xml = new java.lang.String(Xml.writeToString(data)); 383 zip.addData(xml.getBytes("UTF-8"), "data.xml"); 384 zip.close(); 385 return zip; 386 } 387 388 /** 389 * 390 * @param {String} name 391 * @returns {HopObject} 392 */ 393 Layout.prototype.getMacroHandler = function(name) { 394 switch (name) { 395 case "skins": 396 return this[name]; 397 398 default: 399 return null; 400 } 401 } 402 403 /** 404 * 405 * @param {Object} param 406 * @param {String} name 407 * @param {String} mode 408 */ 409 Layout.prototype.image_macro = function(param, name, mode) { 410 name || (name = param.name); 411 if (!name) { 412 return; 413 } 414 415 var image = this.getImage(name, param.fallback); 416 if (!image) { 417 return; 418 } 419 420 mode || (mode = param.as); 421 var action = param.linkto; 422 delete(param.name); 423 delete(param.as); 424 delete(param.linkto); 425 426 switch (mode) { 427 case "url" : 428 return res.write(image.getUrl()); 429 case "thumbnail" : 430 action || (action = image.getUrl()); 431 return image.thumbnail_macro(param); 432 } 433 image.render_macro(param); 434 return; 435 } 436 437 /** 438 * 439 */ 440 Layout.prototype.values_macro = function() { 441 var values = []; 442 for (var key in res.meta.values) { 443 values.push({key: key, value: res.meta.values[key]}); 444 } 445 values.sort(new String.Sorter("key")); 446 for each (var pair in values) { 447 this.renderSkin("$Layout#value", { 448 key: pair.key.capitalize(), 449 value: pair.value 450 }); 451 } 452 return; 453 } 454