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:3333 $
 20 // $LastChangedBy:piefke3000 $
 21 // $LastChangedDate:2007-09-15 01:25:23 +0200 (Sat, 15 Sep 2007) $
 22 // $URL$
 23 //
 24 
 25 /**
 26  * @fileOverview Defines the Admin prototype.
 27  */
 28 
 29 Admin.SITEREMOVALGRACEPERIOD = 14; // days
 30 
 31 /**
 32  * @function
 33  * @returns {String[]}
 34  * @see defineConstants
 35  */
 36 Admin.getNotificationScopes = defineConstants(Admin, "none", "trusted", "regular");
 37 
 38 /**
 39  * @function
 40  * @return {String[]}
 41  * @see defineConstants
 42  */
 43 Admin.getPhaseOutModes = defineConstants(Admin, "disabled", "restricted", "abandoned", "both");
 44 
 45 /**
 46  * @function
 47  * @returns {String[]}
 48  * @see defineConstants
 49  */
 50 Admin.getCreationScopes = defineConstants(Admin, "privileged", "trusted", "regular");
 51 
 52 /**
 53  * 
 54  * @param {Object} job
 55  */
 56 Admin.queue = function(job) {
 57    var file = java.io.File.createTempFile("job-", String.EMPTY, Admin.queue.dir);
 58    serialize(job, file);
 59    return;
 60 }
 61 
 62 /**
 63  * 
 64  */
 65 Admin.queue.dir = (new java.io.File(app.dir, "../jobs")).getCanonicalFile();
 66 Admin.queue.dir.exists() || Admin.queue.dir.mkdirs();
 67 
 68 /**
 69  * 
 70  */
 71 Admin.dequeue = function() {
 72    var jobs = Admin.queue.dir.listFiles();
 73    var max = Math.min(jobs.length, 10);
 74    for (var file, job, i=0; i<max; i+=1) {
 75       file = jobs[i]; 
 76       try {
 77          job = deserialize(file);
 78          app.log("PROCESSING QUEUED JOB " + (i+1) + " OF " + max);
 79          switch (job.type) {
 80             case "site-removal":
 81             var site = Site.getById(job.id);
 82             site && site !== root && Site.remove.call(site);
 83             break;
 84          }
 85       } catch (e) {
 86          app.log("Failed to process job " + file + " due to " + e);
 87       }
 88       file["delete"]();
 89    }
 90    return;
 91 }
 92 
 93 /**
 94  * 
 95  */
 96 Admin.purgeSites = function() {
 97    var now = new Date;
 98 
 99    root.admin.deletedSites.forEach(function() {
100       if (now - this.deleted > Date.ONEDAY * Admin.SITEREMOVALGRACEPERIOD) {
101          Admin.queue({type: "site-removal", id: this._id});
102          this.deleted = now; // Prevents redundant deletion jobs
103       }
104    });
105    
106    var notificationPeriod = root.phaseOutNotificationPeriod * Date.ONEDAY;
107    var gracePeriod = root.phaseOutGracePeriod * Date.ONEDAY;
108 
109    var phaseOutAbandonedSites = function() {
110       root.forEach(function() {
111          if (this.status === Site.TRUSTED) {
112             return;
113          }
114          if (age - notificationPeriod > 0) {
115             if (!this.notified || now - this.notified > notificationPeriod) {
116                var site = this;
117                this.members.owners.forEach(function() {
118                   sendMail(this.creator.email,
119                         gettext("Notification of changes at site {0}", site.title),
120                         site.renderSkinAsString("$Site#notify_deletion"));
121                });
122                this.notified = now;
123             }
124             if (age - notificationPeriod - gracePeriod > 0) {
125                this.mode = Site.DELETED;
126                this.deleted = now;
127             }
128          }
129       });
130       return;
131    }
132    
133    var phaseOutPrivateSites = function() {
134       root.admin.restrictedSites.forEach(function() {
135          if (this.status === Site.TRUSTED) {
136             return;
137          }
138          var age = now - (this.restricted || this.created);
139          if (age - notificationPeriod > 0) {
140             if (!this.notified || now - this.notified > notificationPeriod) {
141                var site = this;
142                this.members.owners.forEach(function() {
143                   sendMail(this.creator.email,
144                         gettext("Notification of changes at site {0}", site.title),
145                         site.renderSkinAsString("$Site#notify_blocking"));
146                });
147                this.notified = now;
148             }
149             if (age - notificationPeriod - gracePeriod > 0) {
150                this.status = Site.BLOCKED;
151             }
152          }
153       });
154       return;
155    }
156    
157    switch (root.phaseOutMode) {
158       case Admin.ABANDONED:
159       return phaseOutAbandonedSites();
160       case Admin.RESTRICTED:
161       return phaseOutPrivateSites();
162       case Admin.BOTH:
163       phaseOutAbandonedSites();
164       return phaseOutPrivateSites();
165    }
166    return;
167 }
168 
169 /**
170  * 
171  */
172 Admin.purgeReferrers = function() {
173    var sql = new Sql;
174    var result = sql.execute("delete from log where action = 'main' and " +
175          "created < date_add(now(), interval -2 day)");
176    return result;
177 }
178 
179 /**
180  * 
181  */
182 Admin.commitRequests = function() {
183    var requests = app.data.requests;
184    app.data.requests = {};
185    for each (var item in requests) {
186       switch (item.type) {
187          case Story:
188          var story = Story.getById(item.id);
189          story && (story.requests = item.requests);
190          break;
191       }
192    }
193    res.commit();
194    return;
195 }
196 
197 /**
198  * 
199  */
200 Admin.commitEntries = function() {
201    var entries = app.data.entries;   
202    app.data.entries = [];
203    var history = [];
204 
205    for each (var item in entries) {
206       var referrer = helma.Http.evalUrl(item.referrer);
207       if (!referrer) {
208          continue;
209       }
210 
211       // Only log unique combinations of context, ip and referrer
212       referrer = String(referrer);
213       var key = item.context_type + "#" + item.context_id + ":" + 
214             item.ip + ":" + referrer;
215       if (history.indexOf(key) > -1) {
216          continue;
217       }
218       history.push(key);
219 
220       // Exclude requests coming from the same site
221       if (item.site) {
222          var href = item.site.href().toLowerCase();
223          if (referrer.toLowerCase().contains(href.substr(0, href.length-1))) {
224             continue;
225          }
226       }
227       item.persist();
228    }
229 
230    res.commit();
231    return;
232 }
233 
234 /**
235  * 
236  */
237 Admin.invokeCallbacks = function() {
238    var http = helma.Http();
239    http.setTimeout(200);
240    http.setReadTimeout(300);
241    http.setMethod("POST");
242 
243    var ref, site, item;
244    while (ref = app.data.callbacks.pop()) {
245       site = Site.getById(ref.site);
246       item = ref.handler && ref.handler.getById(ref.id);
247       if (!site || !item) {
248          continue;
249       }
250       app.log("Invoking callback URL " + site.callbackUrl + " for " + item);
251       try {
252          http.setContent({
253             type: item.constructor.name,
254             id: item.name || item._id,
255             url: item.href(),
256             date: item.modified.valueOf(),
257             user: item.modifier.name,
258             site: site.title || site.name,
259             origin: site.href()
260          });
261          http.getUrl(site.callbackUrl);
262       } catch (ex) {
263          app.debug("Invoking callback URL " + site.callbackUrl + " failed: " + ex);
264       }
265    }
266    return;
267 }
268 
269 /**
270  * 
271  */
272 Admin.updateHealth = function() {
273    var health = Admin.health || {};
274    if (!health.modified || new Date - health.modified > 5 * Date.ONEMINUTE) {
275       health.modified = new Date;
276       health.requestsPerUnit = app.requestCount - 
277             (health.currentRequestCount || 0);
278       health.currentRequestCount = app.requestCount;
279       health.errorsPerUnit = app.errorCount - (health.currentErrorCount || 0);
280       health.currentErrorCount = app.errorCount;
281       Admin.health = health;
282    }
283    return;
284 }
285 
286 /**
287  * 
288  */
289 Admin.exportImport = function() {
290    if (app.data.exportImportIsRunning) {
291       return;
292    }
293    app.invokeAsync(this, function() {
294       app.data.exportImportIsRunning = true;
295       Exporter.run();
296       Importer.run();
297       app.data.exportImportIsRunning = false;
298    }, [], -1);
299    return;
300 }
301 
302 /**
303  * 
304  */
305 Admin.updateDomains = function() {
306    res.push();
307    for (var key in app.properties) {
308       if (key.startsWith("domain.")) {
309          res.writeln(getProperty(key) + "\t\t" + key.substr(7));
310       }
311    }
312    var map = res.pop();
313    var file = new java.io.File(app.dir, "domains.map");
314    var out = new java.io.BufferedWriter(new java.io.OutputStreamWriter(
315          new java.io.FileOutputStream(file), "UTF-8"));
316    out.write(map);
317    out.close();
318    return;
319 }
320 
321 /**
322  * @name Admin
323  * @constructor
324  * @property {LogEntry[]} entries
325  * @property {Sites[]} privateSites
326  * @property {Sites[]} sites
327  * @property {Users[]} users
328  * @extends HopObject
329  */
330 Admin.prototype.constructor = function() {
331    this.filterSites();
332    this.filterUsers();
333    this.filterLog();
334    return this;
335 }
336 
337 /**
338  * 
339  * @param {Object} action
340  * @returns {Boolean}
341  */
342 Admin.prototype.getPermission = function(action) {
343    if (!session.user) {
344       return false;
345    }
346    switch (action) {
347       case "users":
348       if (req.queryParams.id === session.user._id) {
349          return false;
350       }
351       break;
352    }
353    return User.require(User.PRIVILEGED);
354 }
355 
356 /**
357  * 
358  */
359 Admin.prototype.onRequest = function() {
360    HopObject.prototype.onRequest.apply(this);
361    if (!session.data.admin) {
362       session.data.admin = new Admin();
363    }
364    return;
365 }
366 
367 /**
368  * 
369  * @param {String} name
370  */
371 Admin.prototype.onUnhandledMacro = function(name) {
372    res.debug("Add " + name + "_macro to Admin!");
373    return null;
374 }
375 
376 Admin.prototype.main_action = function() {
377    return res.redirect(this.href("log"));
378 }
379 
380 Admin.prototype.setup_action = function() {
381    //Site.remove.call(Site.getByName("deleteme"));
382    
383    if (req.postParams.save) {
384       try {
385          this.update(req.postParams);
386          this.log(root, "setup");
387          res.message = gettext("Successfully updated the setup.");
388          res.redirect(this.href(req.action));
389       } catch (ex) {
390          res.message = ex;
391          app.log(ex);
392       }
393    }
394 
395    res.data.title = gettext("Setup");
396    res.data.action = this.href(req.action);
397    res.data.body = this.renderSkinAsString("$Admin#setup");
398    root.renderSkin("Site#page");
399    return;
400 }
401 
402 /**
403  * 
404  * @param {Object} data
405  */
406 Admin.prototype.update = function(data) {
407    root.map({
408       creationScope: data.creationScope,
409       creationDelay: data.creationDelay,
410       replyTo: data.replyTo,
411       notificationScope: data.notificationScope,
412       phaseOutGracePeriod: data.phaseOutGracePeriod,
413       phaseOutMode: data.phaseOutMode,
414       phaseOutNotificationPeriod: data.phaseOutNotificationPeriod,
415       probationPeriod: data.probationPeriod,
416       quota: data.quota
417    });
418    this.log(root, "Updated setup");
419    return;
420 }
421 
422 Admin.prototype.jobs_action = function() {
423    var files = Admin.queue.dir.listFiles();
424    for each (var file in files) {
425       var job = deserialize(file);
426       res.debug(job.toSource() + " – " + file);
427    }
428    return;
429 }
430 
431 Admin.prototype.log_action = function() {
432    if (req.postParams.search || req.postParams.filter) {
433       session.data.admin.filterLog(req.postParams);
434    }
435    res.data.list = renderList(session.data.admin.entries, 
436          this.renderItem, 10, req.queryParams.page);
437    res.data.pager = renderPager(session.data.admin.entries, 
438          this.href(req.action), 10, req.queryParams.page);
439 
440    res.data.title = gettext("Administration Log");
441    res.data.action = this.href(req.action);
442    res.data.body = this.renderSkinAsString("$Admin#log");
443    res.data.body += this.renderSkinAsString("$Admin#main");
444    root.renderSkin("Site#page");
445    return;
446 }
447 
448 Admin.prototype.sites_action = function() {
449    if (req.postParams.id) {
450       if (req.postParams.remove === "1") {
451          var site = Site.getById(req.postParams.id);
452          site.deleted = new Date;
453          site.status = Site.BLOCKED;
454          site.mode = Site.DELETED;
455          this.log(root, "Deleted site " + site.name);
456          res.message = gettext("The site {0} is queued for removal.",
457                site.name);
458          res.redirect(this.href(req.action) + "?page=" + req.postParams.page);
459       } else if (req.postParams.save === "1") {
460          this.updateSite(req.postParams);
461          res.message = gettext("The changes were saved successfully.");
462       }
463       res.redirect(this.href(req.action) + "?page=" + req.postParams.page + 
464             "#" + req.postParams.id);
465       return;
466    }
467    
468    if (req.postParams.search || req.postParams.filter) {
469       session.data.admin.filterSites(req.postParams);
470    } else if (req.queryParams.id) {
471       res.meta.item = Site.getById(req.queryParams.id);
472    }
473 
474    res.data.list = renderList(session.data.admin.sites, 
475          this.renderItem, 10, req.queryParams.page);
476    res.data.pager = renderPager(session.data.admin.sites, 
477          this.href(req.action), 10, req.data.page);
478 
479    res.data.title = gettext("Site Administration");
480    res.data.action = this.href(req.action);
481    res.data.body = this.renderSkinAsString("$Admin#sites");
482    res.data.body += this.renderSkinAsString("$Admin#main");
483    root.renderSkin("Site#page");
484    return;
485 }
486 
487 Admin.prototype.users_action = function() {
488    if (req.postParams.search || req.postParams.filter) {
489       session.data.admin.filterUsers(req.postParams);
490    } else if (req.postParams.save) {
491       this.updateUser(req.postParams);
492       res.message = gettext("The changes were saved successfully.");
493       res.redirect(this.href(req.action) + "?page=" + req.postParams.page + 
494             "#" + req.postParams.id);
495    } else if (req.queryParams.id) {
496       res.meta.item = User.getById(req.queryParams.id);
497    }
498 
499    res.data.list = renderList(session.data.admin.users, 
500          this.renderItem, 10, req.data.page);
501    res.data.pager = renderPager(session.data.admin.users, 
502          this.href(req.action), 10, req.data.page);
503 
504    res.data.title = gettext("User Administration");
505    res.data.action = this.href(req.action);
506    res.data.body = this.renderSkinAsString("$Admin#users");
507    res.data.body += this.renderSkinAsString("$Admin#main");
508    root.renderSkin("Site#page");
509    return;
510 }
511 
512 /**
513  * 
514  * @param {Object} data
515  */
516 Admin.prototype.filterLog = function(data) {
517    data || (data = {});
518    var sql = "";
519    if (data.filter > 0) {
520       sql += "where context_type = '";
521       switch (data.filter) {
522          case "1":
523          sql += "Root"; break;
524          case "2":
525          sql += "Site"; break;
526          case "3":
527          sql += "User"; break;
528       }
529       sql += "' and ";
530    } else {
531       sql += "where "
532    }
533    sql += "action <> 'main' "; 
534    if (data.query) {
535       var parts = stripTags(data.query).split(" ");
536       var keyword, like;
537       for (var i in parts) {
538          sql += i < 1 ? "and " : "or ";
539          keyword = parts[i].replace(/\*/g, "%");
540          like = keyword.contains("%") ? "like" : "=";
541          sql += "action " + like + " '" + keyword + "' ";
542       }
543    }
544    sql += "order by created "; 
545    (data.dir == 1) || (sql += "desc");
546    this.entries.subnodeRelation = sql;
547    return;
548 }
549 
550 /**
551  * 
552  * @param {Object} data
553  */
554 Admin.prototype.filterSites = function(data) {
555    data || (data = {});
556    var sql;
557    switch (data.filter) {
558       case "1":
559       sql = "where status = 'blocked' "; break;
560       case "2":
561       sql = "where status = 'trusted' "; break;
562       case "3":
563       sql = "where mode = 'open' "; break;
564       case "4":
565       sql = "where mode = 'restricted' "; break;
566       case "5":
567       sql = "where mode = 'public' "; break;
568       case "6":
569       sql = "where mode = 'closed' "; break;
570       case "7":
571       sql = "where mode = 'deleted' "; break;
572       case "0":
573       default:
574       sql = "where true ";
575    }
576    if (data.query) {
577       var parts = stripTags(data.query).split(" ");
578       var keyword, like;
579       for (var i in parts) {
580          sql += i < 1 ? "and " : "or ";
581          keyword = parts[i].replace(/\*/g, "%");
582          like = keyword.contains("%") ? "like" : "=";
583          sql += "(name " + like + " '" + keyword + "') ";
584       }
585    }
586    switch (data.order) {
587       case "1":
588       sql += "group by created order by created "; break;
589       case "2":
590       sql += "group by name order by name "; break;
591       default:
592       sql += "group by modified order by modified "; break;
593    }
594    (data.dir == 1) || (sql += "desc");
595    this.sites.subnodeRelation = sql;
596    return;
597 }
598 
599 /**
600  * 
601  * @param {Object} data
602  */
603 Admin.prototype.filterUsers = function(data) {
604    data || (data = {});
605    var sql;
606    switch (data.filter) {
607       case "1":
608       sql = "where status = 'blocked' "; break;
609       case "2":
610       sql = "where status = 'trusted' "; break;
611       case "3":
612       sql = "where status = 'privileged' "; break;
613       default:
614       sql = "where true "; break;
615    }
616    if (data.query) {
617       var parts = stripTags(data.query).split(" ");
618       var keyword, like;
619       for (var i in parts) {
620          sql += i < 1 ? "and " : "or ";
621          keyword = parts[i].replace(/\*/g, "%");
622          like = keyword.contains("%") ? "like" : "=";
623          if (keyword.contains("@")) {
624             sql += "email " + like + " '" + keyword.replace(/@/g, "") + "' ";
625          } else {
626             sql += "name " + like + " '" + keyword + "' ";
627          }
628       }
629    }
630    switch (data.order) {
631       case "1":
632       sql += "group by created order by created "; break;
633       case "2":
634       sql += "group by created order by name "; break;
635       case "0":
636       default:
637       sql += "group by modified order by modified "; break;
638    }
639    (data.dir == 1) || (sql += "desc");
640    this.users.subnodeRelation = sql;
641    return;
642 }
643 
644 /**
645  * 
646  * @param {Object} data
647  */
648 Admin.prototype.updateSite = function(data) {
649    var site = Site.getById(data.id);
650    if (!site) {
651       throw Error(gettext("Please choose a site you want to edit."));
652    }
653    if (site.status !== data.status) { 
654       var current = site.status;
655       site.status = data.status;
656       this.log(site, "Changed status from " + current + " to " + site.status);
657    }
658    return;
659 }
660 
661 /**
662  * 
663  * @param {Object} data
664  */
665 Admin.prototype.updateUser = function(data) {
666    var user = User.getById(data.id);
667    if (!user) {
668       throw Error(gettext("Please choose a user you want to edit."));
669    }
670    if (user === session.user) {
671       throw Error(gettext("Sorry, you are not allowed to modify your own account."));
672    }
673    if (data.status !== user.status) {
674       var current = user.status;
675       user.status = data.status;
676       this.log(user, "Changed status from " + current + " to " + data.status);
677    }
678    return;
679 }
680 
681 /**
682  * 
683  * @param {HopObject} item
684  */
685 Admin.prototype.renderItem = function(item) {
686    res.handlers.item = item;
687    var name = item._prototype;
688    (name === "Root") && (name = "Site");
689    Admin.prototype.renderSkin("$Admin#" + name);
690    if (item === res.meta.item) {
691       Admin.prototype.renderSkin((req.data.action === "delete" ? 
692             "$Admin#delete" : "$Admin#edit") + name);
693    }
694    return;
695 }
696 
697 /**
698  * 
699  * @param {HopObject} context
700  * @param {String} action
701  */
702 Admin.prototype.log = function(context, action) {
703    var entry = new LogEntry(context, action);
704    this.entries.add(entry);
705    return;
706 }
707 
708 /**
709  * 
710  * @param {Object} param
711  * @param {String} action
712  * @param {Number} id
713  * @param {String} text
714  */
715 Admin.prototype.link_macro = function(param, action, id, text) {
716    switch (action) {
717       case "delete":
718       case "edit":
719       if (req.action === "users" && (id === session.user._id)) {
720          return;
721       }
722       if (req.action === "sites" && (id === root._id)) {
723          return;
724       }
725       text = gettext(action.capitalize());
726       action = req.action + "?action=" + action + "&id=" + id;
727       if (req.queryParams.page) {
728          action += "&page=" + req.queryParams.page;
729       }
730       action += "#" + id;
731       break;
732       default:
733       text = id;
734    }
735    return HopObject.prototype.link_macro.call(this, param, action, text);
736 }
737 
738 /**
739  * 
740  * @param {Object} param
741  * @param {HopObject} object
742  * @param {String} name
743  */
744 Admin.prototype.count_macro = function(param, object, name) {
745    if (!object || !object.size) {
746       return;
747    }
748    switch (name) {
749       case "comments":
750       if (object.constructor === Site) {
751          res.write("FIXME: takes very long... :(");
752          //res.write(object.stories.comments.size());
753       }
754       return;
755    }
756    res.write(object.size());
757    return;
758 }
759 
760 /**
761  * 
762  * @param {Object} param
763  * @param {String} name
764  */
765 Admin.prototype.skin_macro = function(param, name) {
766    if (this.getPermission("main")) {
767       return HopObject.prototype.skin_macro.apply(this, arguments);
768    }
769    return;
770 }
771 
772 /**
773  * 
774  * @param {Object} param
775  * @param {HopObject} object
776  * @param {String} name
777  */
778 Admin.prototype.items_macro = function(param, object, name) {
779    if (!object || !object.size) {
780       return;
781    }
782    var max = Math.min(object.size(), parseInt(param.limit) || 5);
783    for (var i=0; i<max; i+=1) {
784       html.link({href: object.get(i).href()}, "#" + (object.size()-i) + " ");
785    }
786    return;
787 }
788 
789 /**
790  * 
791  * @param {Object} param
792  */
793 Admin.prototype.dropdown_macro = function(param) {
794    if (!param.name || !param.values) {
795       return;
796    }
797    var options = param.values.split(",").map(function(item, index) {
798       return {
799          value: index,
800          display: gettext(item)
801       }
802    });
803    var selectedIndex = req.postParams[param.name];
804    html.dropDown({name: param.name}, options, selectedIndex);
805    return;
806 }
807