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