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 Root prototype. 27 */ 28 29 /** @constant */ 30 Root.VERSION = "1.2-beta"; 31 32 /** 33 * @function 34 * @returns {String[]} 35 * @see defineConstants 36 */ 37 Root.getScopes = defineConstants(Root, markgettext("any site"), 38 markgettext("public sites"), markgettext("trusted sites"), 39 markgettext("no site")); 40 41 this.handleMetadata("notificationScope"); 42 this.handleMetadata("quota"); 43 this.handleMetadata("creationScope"); 44 this.handleMetadata("creationDelay"); 45 this.handleMetadata("qualifyingPeriod"); 46 this.handleMetadata("qualifyingDate"); 47 this.handleMetadata("autoCleanupEnabled"); 48 this.handleMetadata("autoCleanupStartTime"); 49 this.handleMetadata("phaseOutPrivateSites"); 50 this.handleMetadata("phaseOutInactiveSites"); 51 this.handleMetadata("phaseOutNotificationPeriod"); 52 this.handleMetadata("phaseOutGracePeriod"); 53 54 /** 55 * 56 * @param {Story} ref 57 */ 58 Root.restore = function(ref) { 59 var backup; 60 if (backup = session.data.backup) { 61 ref.title = decodeURIComponent(backup.title); 62 ref.text = decodeURIComponent(backup.text); 63 } 64 return ref; 65 } 66 67 /** 68 * 69 */ 70 Root.commitRequests = function() { 71 var requests = app.data.requests; 72 app.data.requests = {}; 73 for each (var item in requests) { 74 switch (item.type) { 75 case Story: 76 var story = Story.getById(item.id); 77 story && (story.requests = item.requests); 78 break; 79 } 80 } 81 res.commit(); 82 return; 83 } 84 85 /** 86 * 87 */ 88 Root.commitEntries = function() { 89 var entries = app.data.entries; 90 if (entries.length < 1) { 91 return; 92 } 93 94 app.data.entries = []; 95 var history = []; 96 97 for each (var item in entries) { 98 var referrer = helma.Http.evalUrl(item.referrer); 99 if (!referrer) { 100 continue; 101 } 102 103 // Only log unique combinations of context, ip and referrer 104 referrer = String(referrer); 105 var key = item.context_type + "#" + item.context_id + ":" + 106 item.ip + ":" + referrer; 107 if (history.indexOf(key) > -1) { 108 continue; 109 } 110 history.push(key); 111 112 // Exclude requests coming from the same site 113 if (item.site) { 114 var href = item.site.href().toLowerCase(); 115 if (referrer.toLowerCase().contains(href.substr(0, href.length-1))) { 116 continue; 117 } 118 } 119 item.persist(); 120 } 121 122 res.commit(); 123 return; 124 } 125 126 /** 127 * 128 */ 129 Root.purgeReferrers = function() { 130 var sql = new Sql; 131 var result = sql.execute("delete from log where action = 'main' and " + 132 "created < date_add(now(), interval -1 day)"); 133 return result; 134 } 135 136 /** 137 * 138 */ 139 Root.invokeCallbacks = function() { 140 var http = helma.Http(); 141 http.setTimeout(200); 142 http.setReadTimeout(300); 143 http.setMethod("POST"); 144 145 var ref, site, item; 146 while (ref = app.data.callbacks.pop()) { 147 site = Site.getById(ref.site); 148 item = ref.handler && ref.handler.getById(ref.id); 149 if (!site || !item) { 150 continue; 151 } 152 app.log("Invoking callback URL " + site.callbackUrl + " for " + item); 153 try { 154 http.setContent({ 155 type: item.constructor.name, 156 id: item.name || item._id, 157 url: item.href(), 158 date: item.modified.valueOf(), 159 user: item.modifier.name, 160 site: site.title || site.name, 161 origin: site.href() 162 }); 163 http.getUrl(site.callbackUrl); 164 } catch (ex) { 165 app.debug("Invoking callback URL " + site.callbackUrl + " failed: " + ex); 166 } 167 } 168 return; 169 } 170 171 /** 172 * 173 */ 174 Root.updateHealth = function() { 175 var health = Root.health || {}; 176 if (!health.modified || new Date - health.modified > 5 * Date.ONEMINUTE) { 177 health.modified = new Date; 178 health.requestsPerUnit = app.requestCount - 179 (health.currentRequestCount || 0); 180 health.currentRequestCount = app.requestCount; 181 health.errorsPerUnit = app.errorCount - (health.currentErrorCount || 0); 182 health.currentErrorCount = app.errorCount; 183 Root.health = health; 184 } 185 return; 186 } 187 188 /** 189 * 190 */ 191 Root.exportImport = function() { 192 if (app.data.exportImportIsRunning) { 193 return; 194 } 195 app.invokeAsync(this, function() { 196 app.data.exportImportIsRunning = true; 197 Exporter.run(); 198 Importer.run(); 199 app.data.exportImportIsRunning = false; 200 }, [], -1); 201 return; 202 } 203 204 /** 205 * Antville’s root object is an extent of the Site prototype. 206 * @name Root 207 * @constructor 208 * @property {Site[]} _children 209 * @property {Admin} admin 210 * @property {User[]} admins 211 * @property {Api} api 212 * @property {String} autoCleanupEnabled 213 * @property {String} autoCleanupStartTime 214 * @property {String} creationDelay 215 * @property {String} creationScope 216 * @property {String} notificationScope 217 * @property {String} phaseOutGracePeriod 218 * @property {String} phaseOutInactiveSites 219 * @property {String} phaseOutNotificationPeriod 220 * @property {String} phaseOutPrivateSites 221 * @property {String} qualifyingDate 222 * @property {String} qualifyingPeriod 223 * @property {String} quote 224 * @property {Site[]} sites 225 * @property {Site[]} updates 226 * @property {User[]} users 227 * @extends Site 228 */ 229 230 /** 231 * 232 * @param {String} href 233 * @returns {String} 234 */ 235 Root.prototype.processHref = function(href) { 236 return app.properties.defaulthost + href; 237 } 238 239 /** 240 * 241 * @param {String} action 242 * @returns {Boolean} 243 */ 244 Root.prototype.getPermission = function(action) { 245 if (action.contains("admin")) { 246 return User.require(User.PRIVILEGED); 247 } 248 switch (action) { 249 case "debug": 250 return true; 251 case "create": 252 case "import": 253 return this.getCreationPermission(); 254 case "default.hook": 255 case "health": 256 case "mrtg": 257 case "sites": 258 case "updates.xml": 259 return this.mode !== Site.CLOSED; 260 } 261 return Site.prototype.getPermission.apply(this, arguments); 262 } 263 264 Root.prototype.main_action = function() { 265 // FIXME: Should this better go into HopObject.onRequest? 266 if (this.users.size() < 1) { 267 root.title = "Antville"; 268 res.redirect(this.members.href("register")); 269 } else if (session.user && this.members.owners.size() < 1) { 270 this.creator = this.modifier = this.layout.creator = 271 this.layout.modifier = session.user; 272 this.created = this.modified = 273 this.layout.created = this.layout.modified = new Date; 274 session.user.role = User.PRIVILEGED; 275 res.handlers.membership.role = Membership.OWNER; 276 } 277 return Site.prototype.main_action.apply(this); 278 } 279 280 /** 281 * 282 * @param {String} name 283 * @returns {Object} 284 * @see Site#getFormOptions 285 */ 286 Root.prototype.getFormOptions = function(name) { 287 switch (name) { 288 case "notificationScope": 289 return Root.getScopes(); 290 case "creationScope": 291 return User.getScopes(); 292 case "autoCleanupStartTime": 293 return Admin.getHours(); 294 return; 295 } 296 return Site.prototype.getFormOptions.apply(this, arguments); 297 } 298 299 Root.prototype.error_action = function() { 300 res.status = 500; 301 res.data.title = root.getTitle() + " - Error"; 302 res.data.body = root.renderSkinAsString("$Root#error", res); 303 res.handlers.site.renderSkin("Site#page"); 304 return; 305 } 306 307 Root.prototype.notfound_action = function() { 308 res.status = 404; 309 res.data.title = root.getTitle() + " - Error"; 310 res.data.body = root.renderSkinAsString("$Root#notfound", req); 311 res.handlers.site.renderSkin("Site#page"); 312 return; 313 } 314 315 Root.prototype.create_action = function() { 316 var site = new Site; 317 if (req.postParams.create) { 318 try { 319 site.update(req.postParams); 320 321 var copy = function(source, target) { 322 source.list().forEach(function(name) { 323 var file = new helma.File(source, name); 324 if (file.isDirectory()) { 325 copy(file, new helma.File(target, name)); 326 } else { 327 target.makeDirectory(); 328 file.hardCopy(new helma.File(target, name)); 329 } 330 }) 331 } 332 copy(root.layout.getFile(), site.layout.getFile()); 333 334 this.add(site); 335 site.members.add(new Membership(session.user, Membership.OWNER)); 336 root.admin.log(site, "Added site"); 337 res.message = gettext("Successfully created your site."); 338 res.redirect(site.href()); 339 } catch (ex) { 340 res.message = ex; 341 app.log(ex); 342 } 343 } 344 345 res.handlers.example = new Site; 346 res.handlers.example.name = "foo"; 347 res.data.action = this.href(req.action); 348 res.data.title = gettext("Create a new site"); 349 res.data.body = site.renderSkinAsString("$Site#create"); 350 root.renderSkin("Site#page"); 351 return; 352 } 353 354 Root.prototype.sites_action = function() { 355 var now = new Date; 356 if (!this.cache.sites || (now - this.cache.sites.modified > Date.ONEHOUR)) { 357 var sites = this.sites.list(); 358 sites.sort(new String.Sorter("title")); 359 this.cache.sites = {list: sites, modified: now}; 360 } 361 res.data.list = renderList(this.cache.sites.list, 362 "$Site#listItem", 25, req.queryParams.page); 363 res.data.pager = renderPager(this.cache.sites.list, 364 this.href(req.action), 25, req.queryParams.page); 365 res.data.title = gettext("Public sites of {0}", root.title); 366 res.data.body = this.renderSkinAsString("$Root#sites"); 367 root.renderSkin("Site#page"); 368 return; 369 } 370 371 Root.prototype.updates_xml_action = function() { 372 var now = new Date; 373 var feed = new rome.SyndFeedImpl(); 374 feed.setFeedType("rss_2.0"); 375 feed.setLink(root.href()); 376 feed.setTitle("Recently updated sites at " + root.title); 377 feed.setDescription(root.tagline); 378 feed.setLanguage(root.locale.replace("_", "-")); 379 feed.setPublishedDate(now); 380 var entries = new java.util.ArrayList(); 381 var entry, description; 382 var sites = root.updates.list(0, 25).sort(Number.Sorter("modified", 383 Number.Sorter.DESC)); 384 for each (var site in sites) { 385 entry = new rome.SyndEntryImpl(); 386 entry.setTitle(site.title); 387 entry.setLink(site.href()); 388 entry.setAuthor(site.creator.name); 389 entry.setPublishedDate(site.modified); 390 description = new rome.SyndContentImpl(); 391 description.setType("text/plain"); 392 description.setValue(site.tagline); 393 entry.setDescription(description); 394 entries.add(entry); 395 } 396 feed.setEntries(entries); 397 var output = new rome.SyndFeedOutput(); 398 //output.output(feed, res.servletResponse.writer); return; 399 var xml = output.outputString(feed); 400 res.contentType = "text/xml"; 401 res.write(xml); //injectXslDeclaration(xml)); 402 return; 403 } 404 405 /** 406 * Sitemap for Google Webmaster Tools 407 * (Unfortunately, utterly useless.) 408 */ 409 Root.prototype.sitemap_xml_action = function() { 410 res.contentType = "text/xml"; 411 res.writeln('<?xml version="1.0" encoding="UTF-8"?>'); 412 res.writeln('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'); 413 this.sites.forEach(function() { 414 res.writeln('<url>'); 415 res.writeln('<loc>' + this.href() + '</loc>'); 416 if (this.modified) { 417 res.writeln('<lastmod>' + this.modified.format("yyyy-MM-dd") + '</lastmod>'); 418 } 419 res.writeln('</url>'); 420 }); 421 res.writeln('</urlset>'); 422 return; 423 } 424 425 Root.prototype.health_action = function() { 426 var jvm = java.lang.Runtime.getRuntime(); 427 var totalMemory = jvm.totalMemory() / 1024 / 1024; 428 var freeMemory = jvm.freeMemory() / 1024 / 1024; 429 430 var param = { 431 uptime: formatNumber((new Date - app.upSince.getTime()) / 432 Date.ONEDAY, "0.##"), 433 freeMemory: formatNumber(freeMemory), 434 totalMemory: formatNumber(totalMemory), 435 usedMemory: formatNumber(totalMemory - freeMemory), 436 sessions: formatNumber(app.countSessions()), 437 cacheSize: formatNumber(getProperty("cacheSize")) 438 }; 439 440 for each (key in ["activeThreads", "freeThreads", "requestCount", 441 "errorCount", "xmlrpcCount", "cacheusage"]) { 442 param[key] = formatNumber(app[key]); 443 } 444 445 if (Root.health) { 446 param.requestsPerUnit = formatNumber(Root.health.requestsPerUnit); 447 param.errorsPerUnit = formatNumber(Root.health.errorsPerUnit); 448 } 449 450 param.entries = app.data.entries.length; 451 param.mails = app.data.mails.length; 452 param.requests = 0; 453 for (var i in app.data.requests) { 454 param.requests += 1; 455 } 456 param.callbacks = app.data.callbacks.length; 457 458 res.data.title = "Health of " + root.getTitle(); 459 res.data.body = this.renderSkinAsString("$Root#health", param); 460 this.renderSkin("Site#page"); 461 } 462 463 Root.prototype.import_action = function() { 464 res.debug(app.data.imports.toSource()) 465 var baseDir = this.getStaticFile(); 466 var importDir = new java.io.File(baseDir, "import"); 467 if (req.postParams.submit === "import") { 468 var data = req.postParams; 469 try { 470 if (!data.file) { 471 throw Error(gettext("Please choose a ZIP file to import")); 472 } 473 var site = new Site; 474 site.update({name: data.name}); 475 site.members.add(new Membership(session.user, Membership.OWNER)); 476 root.add(site); 477 Importer.add(new java.io.File(importDir, data.file), 478 site, session.user); 479 res.message = gettext("Queued import of {0} into site »{1}«", 480 data.file, site.name); 481 res.redirect(this.href(req.action)); 482 } catch (ex) { 483 res.message = ex.toString(); 484 app.log(ex.toString()); 485 } 486 } 487 488 res.push(); 489 for each (var file in importDir.listFiles()) { 490 if (file.toString().endsWith(".zip")) { 491 this.renderSkin("$Root#importItem", { 492 file: file.getName(), 493 status: Importer.getStatus(file) 494 }); 495 } 496 } 497 res.data.body = this.renderSkinAsString("$Root#import", {list: res.pop()}); 498 this.renderSkin("Site#page"); 499 return; 500 } 501 502 Root.prototype.mrtg_action = function() { 503 res.contentType = "text/plain"; 504 switch (req.queryParams.target) { 505 case "cache": 506 res.writeln(0); 507 res.writeln(app.cacheusage * 100 / getProperty("cacheSize")); 508 break; 509 case "threads": 510 res.writeln(0); 511 res.writeln(app.activeThreads * 100 / app.freeThreads); 512 break; 513 case "requests": 514 res.writeln(app.errorCount); 515 res.writeln(app.requestCount); 516 break; 517 case "users": 518 res.writeln(app.countSessions()); 519 res.writeln(root.users.size()); 520 break; 521 case "postings": 522 var db = getDBConnection("antville"); 523 var postings = db.executeRetrieval("select count(*) as count from content"); 524 postings.next(); 525 res.writeln(0); 526 res.writeln(postings.getColumnItem("count")); 527 postings.release(); 528 break; 529 case "uploads": 530 var db = getDBConnection("antville"); 531 var files = db.executeRetrieval("select count(*) as count from file"); 532 var images = db.executeRetrieval("select count(*) as count from image"); 533 files.next(); 534 images.next() 535 res.writeln(files.getColumnItem("count")); 536 res.writeln(images.getColumnItem("count")); 537 files.release(); 538 images.release(); 539 break; 540 } 541 res.writeln(app.upSince); 542 res.writeln("mrtg." + req.queryParams.target + " of Antville version " + Root.VERSION); 543 return; 544 } 545 546 /** 547 * 548 * @param {String} name 549 * @returns {HopObject} 550 * @see Site#getMacroHandler 551 */ 552 Root.prototype.getMacroHandler = function(name) { 553 switch (name) { 554 case "admin": 555 case "api": 556 case "sites": 557 return this[name]; 558 } 559 return Site.prototype.getMacroHandler.apply(this, arguments); 560 } 561 562 /** 563 * @returns {Boolean} 564 */ 565 Root.prototype.getCreationPermission = function() { 566 var user; 567 if (!(user = session.user)) { 568 return false; 569 } if (User.require(User.PRIVILEGED)) { 570 return true; 571 } 572 573 switch (root.creationScope) { 574 case User.PRIVILEGEDUSERS: 575 return false; 576 case User.TRUSTEDUSERS: 577 return User.require(User.TRUSTED); 578 default: 579 case User.ALLUSERS: 580 if (root.qualifyingPeriod) { 581 var days = Math.floor((new Date - user.created) / Date.ONEDAY); 582 if (days < root.qualifyingPeriod) { 583 //throw Error(gettext("Sorry, you have to be a member for at " + 584 // "least {0} days to create a new site.", 585 // formatDate(root.qualifyingPeriod)); 586 return false; 587 } 588 } else if (root.qualifyingDate) { 589 if (user.created > root.qualifyingDate) { 590 //throw Error(gettext("Sorry, only members who have registered " + 591 // "before {0} are allowed to create a new site.", 592 // formatDate(root.qualifyingDate)); 593 return false; 594 } 595 } 596 if (user.sites.count() > 0) { 597 var days = Math.floor((new Date - user.sites.get(0).created) / 598 Date.ONEDAY); 599 if (days < root.creationDelay) { 600 //throw Error(gettext("Sorry, you still have to wait {0} days " + 601 // "before you can create another site.", 602 // root.creationDelay - days)); 603 return false; 604 } 605 } 606 } 607 return true; 608 } 609 610 /** 611 * This method is called from the build script to extract gettext call strings 612 * from scripts and skins. 613 * @param {String} script 614 * @param {String} scanDirs 615 * @param {String} potFile 616 */ 617 Root.prototype.xgettext = function(script, scanDirs, potFile) { 618 var temp = {print: global.print, readFile: global.readFile}; 619 global.print = function(str) {app.log(str);} 620 global.readFile = function(fpath, encoding) { 621 return (new helma.File(fpath)).readAll({charset: encoding || "UTF-8"}); 622 } 623 var args = ["-o", potFile, "-p", "internal"]; 624 for each (var dir in scanDirs.split(" ")) { 625 args.push(app.dir + "/../" + dir); 626 } 627 var file = new helma.File(script); 628 var MessageParser = new Function(file.readAll()); 629 MessageParser.apply(global, args); 630 global.print = temp.print; 631 global.readFile = temp.readFile; 632 return; 633 } 634