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