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 && 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, dir, file; 330 for (var name in skinFiles) { 331 if (content = skinFiles[name][name]) { 332 dir = this.getFile(name); 333 file = new helma.File(dir, name + ".skin"); 334 dir.makeDirectory(); 335 file.open(); 336 file.write(content); 337 file.close(); 338 } 339 } 340 341 // FIXME: Reset the Site skin of root separately 342 content = skinFiles.Root.Site; 343 file = new helma.File(this.getFile("Root"), "Site.skin"); 344 dir.makeDirectory(); 345 file.open(); 346 file.write(content); 347 file.close() 348 349 this.touch(); 350 return; 351 } 352 353 /** 354 * 355 * @param {String} skinPath 356 * @returns {helma.Zip} 357 */ 358 Layout.prototype.getArchive = function(skinPath) { 359 var zip = new helma.Zip(); 360 var skinFiles = app.getSkinfilesInPath(skinPath); 361 for (var name in skinFiles) { 362 if (skinFiles[name][name]) { 363 var file = new helma.File(this.getFile(name), name + ".skin"); 364 if (file.exists()) { 365 zip.add(file, name); 366 } 367 } 368 } 369 370 // FIXME: Add the Site skin of root separately 371 file = new helma.File(this.getFile("Root"), "Site.skin"); 372 file.exists() && zip.add(file, "Root"); 373 374 var data = new HopObject; 375 data.images = new HopObject; 376 this.images.forEach(function() { 377 zip.add(this.getFile()); 378 try { 379 zip.add(this.getThumbnailFile()); 380 } catch (ex) { 381 /* Most likely the thumbnail file is identical to the image */ 382 } 383 var image = new HopObject; 384 for each (var key in Image.KEYS) { 385 image[key] = this[key]; 386 data.images.add(image); 387 } 388 }); 389 390 data.version = Root.VERSION; 391 data.origin = this.origin || this.site.href(); 392 data.originator = this.originator || session.user.name; 393 data.originated = this.originated || new Date; 394 395 // FIXME: XML encoder is losing all mixed-case properties :( 396 var xml = new java.lang.String(Xml.writeToString(data)); 397 zip.addData(xml.getBytes("UTF-8"), "data.xml"); 398 zip.close(); 399 return zip; 400 } 401 402 403 /** 404 * @returns {String} 405 */ 406 Layout.prototype.getConfirmText = function() { 407 return gettext("You are about to reset the layout of site {0}.", 408 this.site.name); 409 } 410 /** 411 * 412 * @param {String} name 413 * @returns {HopObject} 414 */ 415 Layout.prototype.getMacroHandler = function(name) { 416 switch (name) { 417 case "skins": 418 return this[name]; 419 420 default: 421 return null; 422 } 423 } 424 425 /** 426 * 427 * @param {Object} param 428 * @param {String} name 429 * @param {String} mode 430 */ 431 Layout.prototype.image_macro = function(param, name, mode) { 432 name || (name = param.name); 433 if (!name) { 434 return; 435 } 436 437 var image = this.getImage(name, param.fallback); 438 if (!image) { 439 return; 440 } 441 442 mode || (mode = param.as); 443 var action = param.linkto; 444 delete(param.name); 445 delete(param.as); 446 delete(param.linkto); 447 448 switch (mode) { 449 case "url" : 450 return res.write(image.getUrl()); 451 case "thumbnail" : 452 action || (action = image.getUrl()); 453 return image.thumbnail_macro(param); 454 } 455 image.render_macro(param); 456 return; 457 } 458 459 /** 460 * 461 */ 462 Layout.prototype.values_macro = function() { 463 var values = []; 464 for (var key in res.meta.values) { 465 values.push({key: key, value: res.meta.values[key]}); 466 } 467 values.sort(new String.Sorter("key")); 468 for each (var pair in values) { 469 this.renderSkin("$Layout#value", { 470 key: pair.key.capitalize(), 471 value: pair.value 472 }); 473 } 474 return; 475 } 476