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