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