      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("Insufficient 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  * Convenience method for easily queueing jobs.
    111  * @param {HopObject} target
    112  * @param {String} method
    113  * @param {User} user
    114  * @returns {String}
    115  * @see Admin.Job
    116  */
    117 Admin.queue = function(target, method, user) {
    118    var job = new Admin.Job(target, method, user || session.user);
    119    return job.name;
    120 }
    121
    122 /**
    123  *
    124  */
    125 Admin.queue.dir = (new java.io.File(app.dir, "../jobs")).getCanonicalFile();
    126 Admin.queue.dir.exists() || Admin.queue.dir.mkdirs();
    127
    128 /**
    129  *
    130  */
    131 Admin.dequeue = function() {
    132    var jobs = Admin.queue.dir.list();
    133    var max = Math.min(jobs.length, 10);
    134    for (let i=0; i<max; i+=1) {
    135       let job = new Admin.Job(jobs[i]);
    136       if (job.target) {
    137          try {
    138             app.log("PROCESSING QUEUED JOB " + (i+1) + " OF " + max);
    139             switch (job.method) {
    140                case "remove":
    141                Site.remove.call(job.target);
    142                break;
    143                case "import":
    144                Importer.run(job.target, job.user);
    145                break;
    146                case "export":
    147                Exporter.run(job.target, job.user);
    148                break;
    149             }
    150          } catch (ex) {
    151             app.log("Failed to process job " + job + " due to " + ex);
    152             app.debug(ex.rhinoException);
    153          }
    154       }
    155       job.remove();
    156    }
    157    return;
    158 }
    159
    160 /**
    161  *
    162  */
    163 Admin.purgeSites = function() {
    164    var now = new Date;
    165
    166    root.admin.deletedSites.forEach(function() {
    167       if (now - this.deleted > Date.ONEDAY * Admin.SITEREMOVALGRACEPERIOD) {
    168          if (this.job) {
    169             return; // Site is already scheduled for deletion
    170          }
    171          let job = new Admin.Job(this, "remove", User.getById(1));
    172          this.job = job.name;
    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