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