Fossil

Check-in [f8927901]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Rough and untested implementation for forum display and reply. Add two new capabilities for posting to the forum not subject to moderation, and for the ability to edit posts from others.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | forum-brainstorm-1
Files: files | file ages | folders
SHA3-256:f8927901c2c6579c8f926e38aa136ba257df466178b1e5887e05ac1498c766cd
User & Date: drh 2018-06-15 20:48:18
Context
2018-06-16
13:36
Progress toward getting the forum to actually work. This is an incremental check-in. check-in: 4814c41a user: drh tags: forum-brainstorm-1
2018-06-15
20:48
Rough and untested implementation for forum display and reply. Add two new capabilities for posting to the forum not subject to moderation, and for the ability to edit posts from others. check-in: f8927901 user: drh tags: forum-brainstorm-1
2018-06-14
19:17
This code demonstrates ideas on how to implement a Forum feature in Fossil. This is just ideas - it is not even a working prototype. This change was originally stashed, but then I thought it better to check it in on a branch for the historical record. check-in: 1e363739 user: drh tags: forum-brainstorm-1
Changes
Hide Diffs Side-by-Side Diffs Ignore Whitespace Patch

Changes to src/forum.c.

    23     23   
    24     24   /*
    25     25   ** The schema for the tables that manage the forum, if forum is
    26     26   ** enabled.
    27     27   */
    28     28   static const char zForumInit[] = 
    29     29   @ CREATE TABLE repository.forumpost(
    30         -@   mpostid INTEGER PRIMARY KEY,  -- unique id for each post
           30  +@   mpostid INTEGER PRIMARY KEY,  -- unique id for each post (local)
           31  +@   mposthash TEXT,               -- uuid for this post
    31     32   @   mthreadid INTEGER,            -- thread to which this post belongs
    32     33   @   uname TEXT,                   -- name of user
    33     34   @   mtime REAL,                   -- julian day number
    34         -@   mstatus TEXT,                 -- status.  ('mod','ok')
           35  +@   mstatus TEXT,                 -- status.  NULL=ok. 'mod'=pending moderation
    35     36   @   mimetype TEXT,                -- Mimetype for mbody
    36     37   @   ipaddr TEXT,                  -- IP address of post origin
    37     38   @   inreplyto INT,                -- Parent posting
    38     39   @   mbody TEXT                    -- Content of the post
    39     40   @ );
    40     41   @ CREATE INDEX repository.forumpost_x1 ON
    41         -@   forumpost(threadid,inreplyto,mtime);
    42         -@ CREATE INDEX repository.forumpost_x2 ON
    43         -@   forumpost(mtime) WHERE mstatus='mod';
           42  +@   forumpost(inreplyto,mtime);
    44     43   @ CREATE TABLE repository.forumthread(
    45     44   @   mthreadid INTEGER PRIMARY KEY,
           45  +@   mthreadhash TEXT,             -- uuid for this thread
    46     46   @   mtitle TEXT,                  -- Title or subject line
    47     47   @   mtime REAL,                   -- Most recent update
    48     48   @   npost INT                     -- Number of posts on this thread
    49     49   @ );
    50     50   ;
    51     51   
    52     52   /*
................................................................................
    60     60   }
    61     61   
    62     62   /*
    63     63   ** WEBPAGE: forum
    64     64   ** URL: /forum
    65     65   ** Query parameters:
    66     66   **
    67         -**    thread=N           Show posts from thread N
    68     67   **    item=N             Show post N and its replies
    69     68   **    
    70     69   */
    71     70   void forum_page(void){
    72         -  int threadId = 0;
    73     71     int itemId;
    74     72     Stmt q;
    75     73     int i;
    76     74   
    77     75     login_check_credentials();
    78     76     if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
    79     77     forum_verify_schema();
           78  +  style_header("Forum");
    80     79     itemId = atoi(PD("item","0"));
    81     80     if( itemId>0 ){
    82         -    threadId = db_int(0, "SELECT mthreadid FROM forumpost WHERE mpostid=%d",
    83         -                      itemId);
           81  +    double rNow = db_double(0.0, "SELECT julianday('now')");
           82  +    /* Show the post given by itemId and all its descendents */
           83  +    db_prepare(&q,
           84  +      "WITH RECURSIVE"
           85  +      " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS ("
           86  +      "    SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody,"
           87  +      "           0, 1 FROM forumpost WHERE mpostid=%d"
           88  +      "  UNION"
           89  +      "  SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr,"
           90  +      "         f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime"
           91  +      "    FROM forumpost AS f, post AS p"
           92  +      "   WHERE forumpost.inreplyto=post.id"
           93  +      "   ORDER BY xdepth DESC, xtime ASC"
           94  +      ") SELECT * FROM post;",
           95  +      itemId
           96  +    );
           97  +    @ <table border=0 class="forumtable">
           98  +    while( db_step(&q)==SQLITE_ROW ){
           99  +      int id = db_column_int(&q, 0);
          100  +      const char *zUser = db_column_text(&q, 1);
          101  +      const char *zStat = db_column_text(&q, 2);
          102  +      const char *zMime = db_column_text(&q, 3);
          103  +      const char *zIp = db_column_text(&q, 4);
          104  +      int iDepth = db_column_int(&q, 7);
          105  +      double rMTime = db_column_double(&q, 8);
          106  +      char *zAge = db_timespan_name(rNow - rMTime);
          107  +      Blob body;
          108  +      @ <!-- Forum post %d(id) -->
          109  +      @ <tr>
          110  +      @ <td class="forum_margin" width="%d((iDepth-1)*10)" rowspan="3"></td>
          111  +      @ <td>%h(zUser) %z(zAge) ago</td>
          112  +      @ </tr>
          113  +      @ <tr><td class="forum_body">
          114  +      blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6));
          115  +      wiki_render_by_mimetype(&body, zMime);
          116  +      blob_reset(&body);
          117  +      @ </td></tr>
          118  +      @ <tr><td class="forum_buttons">
          119  +      if( g.perm.WrForum ){
          120  +        if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){
          121  +          @ <a href='%R/forumedit?item=%d(id)'>Edit</a>
          122  +        }
          123  +        @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a>
          124  +      }
          125  +      @ </td></tr>
          126  +    }
          127  +    @ </table>
          128  +  }else{
          129  +    /* If we reach this point, that means the users wants a list of
          130  +    ** recent threads.
          131  +    */
          132  +    i = 0;
          133  +    db_prepare(&q,
          134  +      "SELECT a.mtitle, a.npost, b.mpostid"
          135  +      "  FROM forumthread AS a, forumpost AS b "
          136  +      " WHERE a.mthreadid=b.mthreadid"
          137  +      "   AND b.inreplyto IS NULL"
          138  +      " ORDER BY a.mtime DESC LIMIT 40"
          139  +    );
          140  +    if( g.perm.WrForum ){
          141  +      style_submenu_element("New", "%R/forumedit");
          142  +    }
          143  +    @ <h1>Recent Forum Threads</h>
          144  +    while( db_step(&q)==SQLITE_OK ){
          145  +      int n = db_column_int(&q,1);
          146  +      int itemid = db_column_int(&q,2);
          147  +      const char *zTitle = db_column_text(&q,0);
          148  +      if( i==0 ){
          149  +        @ <ol>
          150  +      }
          151  +      @ <li>
          152  +      @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a><br>
          153  +      @ %d(n) post%s(n==1?"":"s")</li>
          154  +    }
          155  +    if( i ){
          156  +      @ </ol>
          157  +    }
          158  +  }
          159  +  style_footer();
          160  +}
          161  +
          162  +/*
          163  +** Use content in CGI parameters "s" (subject), "b" (body), and
          164  +** "m" (mimetype) to create a new forum entry.
          165  +** Return the id of the new forum entry.
          166  +**
          167  +** If any problems occur, return 0 and set *pzErr to a description of
          168  +** the problem.
          169  +**
          170  +** Cases:
          171  +**
          172  +**    itemId==0 && parentId==0        Starting a new thread.
          173  +**    itemId==0 && parentId>0         New reply to parentId
          174  +**    itemId>0 && parentId==0         Edit existing post itemId
          175  +*/
          176  +static int forum_post(int itemId, int parentId, char **pzErr){
          177  +  const char *zSubject = 0;
          178  +  int threadId;
          179  +  double rNow = db_double(0.0, "SELECT julianday('now')");
          180  +  if( itemId==0 && parentId==0 ){
          181  +    /* Start a new thread.  Subject required. */
          182  +    sqlite3_uint64 r1, r2;
          183  +    zSubject = PT("s");
          184  +    if( zSubject==0 || zSubject[0]==0 ){
          185  +      *pzErr = "\"Subject\" required to start a new thread";
          186  +      return 0;
          187  +    }
          188  +    sqlite3_randomness(sizeof(r1), &r1);
          189  +    sqlite3_randomness(sizeof(r2), &r2);
          190  +    db_multi_exec(
          191  +      "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)"
          192  +      "VALUES(lower(hex(randomblob(32))),%Q,%!.17g,1)",
          193  +      zSubject, rNow
          194  +    );
          195  +    threadId = db_last_insert_rowid();
          196  +  }
          197  +  if( itemId ){
          198  +    db_multi_exec(
          199  +       "UPDATE forumpost SET"
          200  +       " mtime=%!.17g,"
          201  +       " mimetype=%Q,"
          202  +       " ipaddr=%Q,"
          203  +       " mbody=%Q"
          204  +       " WHERE mpostid=%d",
          205  +       rNow, PT("m"), P("REMOTE_ADDR"), PT("b"), itemId
          206  +    );
          207  +  }else{
          208  +    db_multi_exec(
          209  +       "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime,"
          210  +       "  mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES"
          211  +       "  (lower(hex(randomblob(32))),%d,%Q,%!.17g,%Q,%Q,%Q,NULL,%Q)",
          212  +       threadId,g.zLogin,rNow,NULL,P("m"),P("REMOTE_ADDR"),P("b"));
          213  +    itemId = db_last_insert_rowid();
          214  +  }
          215  +  if( zSubject==0 ){
          216  +    db_multi_exec(
          217  +      "UPDATE forumthread SET mtime=%!.17g"
          218  +      " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)",
          219  +      rNow, itemId
          220  +    );
          221  +  }
          222  +  return itemId;
          223  +}
          224  +
          225  +/*
          226  +** WEBPAGE: forumedit
          227  +**
          228  +** Query parameters:
          229  +**
          230  +**    replyto=N      Enter a reply to forum item N
          231  +**    item=N         Edit item N
          232  +**    s=SUBJECT      Subject. New thread only. Omitted for replies
          233  +**    b=BODY         Body of the post
          234  +**    m=MIMETYPE     Mimetype for the body of the post
          235  +**    x              Submit changes
          236  +**    p              Preview changes
          237  +*/
          238  +static void forum_reply_page(void){
          239  +  int itemId;
          240  +  int parentId;
          241  +  const char *zErr = 0;
          242  +  login_check_credentials();
          243  +  const char *zBody;
          244  +  const char *zMime;
          245  +  const char *zSub;
          246  +  if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
          247  +  forum_verify_schema();
          248  +  itemId = atoi(PD("item","0"));
          249  +  parentId = atoi(PD("replyto","0"));
          250  +  if( P("x")!=0 && cgi_csrf_safe(1) ){
          251  +    itemId = forum_post(itemId,parentId,&zErr);
          252  +    if( itemId ){
          253  +      cgi_redirectf("%R/forum?item=%d",itemId);
          254  +      return;
          255  +    }
          256  +  }
          257  +  style_header("Edit Forum Post");
          258  +  @ <form method="POST">
          259  +  if( itemId ){
          260  +    @ <input type="hidden" name="item" value="%d(itemId)">
    84    261     }
    85         -  if( threadId==0 ) threadId = atoi(PD("thread","0"));
    86         -  if( threadId>0 ){
    87         -    
          262  +  if( parentId ){
          263  +    @ <input type="hidden" name="replyto" value="%d(parentId)">
    88    264     }
    89         -  i = 0;
    90         -  db_prepare(&q,
    91         -    "SELECT mtitle, npost, mthreadid FROM forumthread"
    92         -    " WHERE inreplyto IS NULL ORDER BY mtime DESC LIMIT 40"
    93         -  );
    94         -  style_header("Recent Forum Threads");
    95         -  while( db_step(&q)==SQLITE_OK ){
    96         -    int n = db_column_int(&q,1);
    97         -    int threadid = db_column_int(&q,2);
    98         -    const char *zTitle = db_column_text(&q,0);
    99         -    if( i==0 ){
   100         -      @ <ol>
          265  +  if( P("p") ){
          266  +    Blob x;
          267  +    @ <div class="forumpreview">
          268  +    if( P("s") ){
          269  +      @ <h1>%h(PT("s"))</h1>
   101    270       }
   102         -    @ <li>
   103         -    @ %z(href("%R/forum?thread=%d",threadid))%h(zTitle)</a><br>
   104         -    @ %d(n) post%s(n==1?"":"s")</li>
          271  +    @ <div class="forumpreviewbody">
          272  +    blob_init(&x, PT("b"), -1);
          273  +    wiki_render_by_mimetype(&x, PT("m"));
          274  +    blob_reset(&x);
          275  +    @ </div>
          276  +    @ </div>
          277  +  }
          278  +  @ <table border="0" class="forumeditform"> 
          279  +  if( itemId==0 && parentId==0 ){
          280  +    zSub = PT("s");
   105    281     }
   106         -  if( i ){
   107         -    @ </ol>
   108         -  }
          282  +  @ </table>
          283  +  @ </form>
   109    284     style_footer();
   110    285   }

Changes to src/login.c.

  1195   1195         case 'a':   p->Admin = p->RdTkt = p->WrTkt = p->Zip =
  1196   1196                                p->RdWiki = p->WrWiki = p->NewWiki =
  1197   1197                                p->ApndWiki = p->Hyperlink = p->Clone =
  1198   1198                                p->NewTkt = p->Password = p->RdAddr =
  1199   1199                                p->TktFmt = p->Attach = p->ApndTkt =
  1200   1200                                p->ModWiki = p->ModTkt = p->Delete =
  1201   1201                                p->RdForum = p->WrForum = p->ModForum =
         1202  +                             p->WrTForum = p->AdminForum =
  1202   1203                                p->WrUnver = p->Private = 1;
  1203   1204                                /* Fall thru into Read/Write */
  1204   1205         case 'i':   p->Read = p->Write = 1;                      break;
  1205   1206         case 'o':   p->Read = 1;                                 break;
  1206   1207         case 'z':   p->Zip = 1;                                  break;
  1207   1208   
  1208   1209         case 'd':   p->Delete = 1;                               break;
................................................................................
  1224   1225         case 'c':   p->ApndTkt = 1;                              break;
  1225   1226         case 'q':   p->ModTkt = 1;                               break;
  1226   1227         case 't':   p->TktFmt = 1;                               break;
  1227   1228         case 'b':   p->Attach = 1;                               break;
  1228   1229         case 'x':   p->Private = 1;                              break;
  1229   1230         case 'y':   p->WrUnver = 1;                              break;
  1230   1231   
         1232  +      case '6':   p->AdminForum = 1;
         1233  +      case '5':   p->ModForum = 1;
         1234  +      case '4':   p->WrTForum = 1;
         1235  +      case '3':   p->WrForum = 1;
  1231   1236         case '2':   p->RdForum = 1;                              break;
  1232         -      case '3':   p->WrForum = 1;                              break;
  1233         -      case '4':   p->ModForum = 1;                             break;
  1234   1237   
  1235   1238         /* The "u" privileges is a little different.  It recursively
  1236   1239         ** inherits all privileges of the user named "reader" */
  1237   1240         case 'u': {
  1238   1241           if( (flags & LOGIN_IGNORE_UV)==0 ){
  1239   1242             const char *zUser;
  1240   1243             zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");

Changes to src/main.c.

    81     81     char ModTkt;           /* q: approve and publish ticket changes (Moderator) */
    82     82     char Attach;           /* b: add attachments */
    83     83     char TktFmt;           /* t: create new ticket report formats */
    84     84     char RdAddr;           /* e: read email addresses or other private data */
    85     85     char Zip;              /* z: download zipped artifact via /zip URL */
    86     86     char Private;          /* x: can send and receive private content */
    87     87     char WrUnver;          /* y: can push unversioned content */
    88         -  char RdForum;          /* 2: Read forum posts and comments */
    89         -  char WrForum;          /* 3: Create new forum posts and comments */
    90         -  char ModForum;         /* 4: Moderate forum posts and comments */
           88  +  char RdForum;          /* 2: Read forum posts */
           89  +  char WrForum;          /* 3: Create new forum posts */
           90  +  char WrTForum;         /* 4: Post to forums not subject to moderation */
           91  +  char ModForum;         /* 5: Moderate (approve or reject) forum posts */
           92  +  char AdminForum;       /* 6: Edit forum posts by other users */
    91     93   };
    92     94   
    93     95   #ifdef FOSSIL_ENABLE_TCL
    94     96   /*
    95     97   ** All Tcl related context information is in this structure.  This structure
    96     98   ** definition has been copied from and should be kept in sync with the one in
    97     99   ** "th_tcl.c".

Changes to src/setup.c.

   345    345        @ <tr><th valign="top">x</th>
   346    346        @   <td><i>Private:</i> Push and/or pull private branches</td></tr>
   347    347        @ <tr><th valign="top">y</th>
   348    348        @   <td><i>Write-Unver:</i> Push unversioned files</td></tr>
   349    349        @ <tr><th valign="top">z</th>
   350    350        @   <td><i>Zip download:</i> Download a ZIP archive or tarball</td></tr>
   351    351        @ <tr><th valign="top">2</th>
   352         -     @   <td><i>Forum-Read:</i> Read Forum posts by others </td></tr>
          352  +     @   <td><i>Forum-Read:</i> Read forum posts by others </td></tr>
   353    353        @ <tr><th valign="top">3</th>
   354         -     @   <td><i>Forum-Append:</i> Add new Forum posts or comments</td></tr>
          354  +     @   <td><i>Forum-Append:</i> Add new forum posts</td></tr>
   355    355        @ <tr><th valign="top">4</th>
   356         -     @   <td><i>Forum-Moderator:</i> Moderate Forums</td></tr>
          356  +     @   <td><i>Forum-Trusted:</i> Add pre-approved forum posts </td></tr>
          357  +     @ <tr><th valign="top">5</th>
          358  +     @   <td><i>Forum-Moderator:</i> Approve or disapprove forum posts</td></tr>
          359  +     @ <tr><th valign="top">6</th>
          360  +     @   <td><i>Forum-Supervisor:</i> \
          361  +     @ Edit forum posts submitted by others</td></tr>
   357    362     @ </table>
   358    363   }
   359    364   
   360    365   /*
   361    366   ** WEBPAGE: setup_ulist_notes
   362    367   **
   363    368   ** A documentation page showing notes about user configuration.  This information
................................................................................
   676    681     @ <table border=0><tr><td valign="top">
   677    682     if( g.perm.Setup ){
   678    683       @  <label><input type="checkbox" name="as"%s(oa['s']) />
   679    684       @  Setup%s(B('s'))</label><br />
   680    685     }
   681    686     @  <label><input type="checkbox" name="aa"%s(oa['a']) />
   682    687     @  Admin%s(B('a'))</label><br />
          688  +  @  <label><input type="checkbox" name="au"%s(oa['u']) />
          689  +  @  Reader%s(B('u'))</label><br>
          690  +  @  <label><input type="checkbox" name="av"%s(oa['v']) />
          691  +  @  Developer%s(B('v'))</label><br />
   683    692     @  <label><input type="checkbox" name="ad"%s(oa['d']) />
   684    693     @  Delete%s(B('d'))</label><br />
   685    694     @  <label><input type="checkbox" name="ae"%s(oa['e']) />
   686    695     @  Email%s(B('e'))</label><br />
   687    696     @  <label><input type="checkbox" name="ap"%s(oa['p']) />
   688    697     @  Password%s(B('p'))</label><br />
   689    698     @  <label><input type="checkbox" name="ai"%s(oa['i']) />
   690    699     @  Check-In%s(B('i'))</label><br />
   691    700     @  <label><input type="checkbox" name="ao"%s(oa['o']) />
   692    701     @  Check-Out%s(B('o'))</label><br />
   693    702     @  <label><input type="checkbox" name="ah"%s(oa['h']) />
   694    703     @  Hyperlinks%s(B('h'))</label><br />
   695    704     @  <label><input type="checkbox" name="ab"%s(oa['b']) />
   696         -  @  Attachments%s(B('b'))</label><br />
   697         -  @  <label><input type="checkbox" name="au"%s(oa['u']) />
   698         -  @  Reader%s(B('u'))</label>
          705  +  @  Attachments%s(B('b'))</label>
          706  +
   699    707     @ </td><td><td width="40"></td><td valign="top">
   700         -  @  <label><input type="checkbox" name="av"%s(oa['v']) />
   701         -  @  Developer%s(B('v'))</label><br />
   702    708     @  <label><input type="checkbox" name="ag"%s(oa['g']) />
   703    709     @  Clone%s(B('g'))</label><br />
   704    710     @  <label><input type="checkbox" name="aj"%s(oa['j']) />
   705         -  @  Read Wiki%s(B('j'))</label><br />
          711  +  @  Read Wiki%s(B('j'))</label><br>
   706    712     @  <label><input type="checkbox" name="af"%s(oa['f']) />
   707    713     @  New Wiki%s(B('f'))</label><br />
   708    714     @  <label><input type="checkbox" name="am"%s(oa['m']) />
   709    715     @  Append Wiki%s(B('m'))</label><br />
   710    716     @  <label><input type="checkbox" name="ak"%s(oa['k']) />
   711    717     @  Write Wiki%s(B('k'))</label><br />
   712    718     @  <label><input type="checkbox" name="al"%s(oa['l']) />
   713    719     @  Moderate Wiki%s(B('l'))</label><br />
   714    720     @  <label><input type="checkbox" name="ar"%s(oa['r']) />
   715    721     @  Read Ticket%s(B('r'))</label><br />
   716    722     @  <label><input type="checkbox" name="an"%s(oa['n']) />
   717    723     @  New Tickets%s(B('n'))</label><br />
   718    724     @  <label><input type="checkbox" name="ac"%s(oa['c']) />
   719         -  @  Append To Ticket%s(B('c'))</label>
   720         -  @ </td><td><td width="40"></td><td valign="top">
          725  +  @  Append To Ticket%s(B('c'))</label><br>
   721    726     @  <label><input type="checkbox" name="aw"%s(oa['w']) />
   722    727     @  Write Tickets%s(B('w'))</label><br />
   723    728     @  <label><input type="checkbox" name="aq"%s(oa['q']) />
   724         -  @  Moderate Tickets%s(B('q'))</label><br />
          729  +  @  Moderate Tickets%s(B('q'))</label>
          730  +
          731  +  @ </td><td><td width="40"></td><td valign="top">
   725    732     @  <label><input type="checkbox" name="at"%s(oa['t']) />
   726    733     @  Ticket Report%s(B('t'))</label><br />
   727    734     @  <label><input type="checkbox" name="ax"%s(oa['x']) />
   728    735     @  Private%s(B('x'))</label><br />
   729    736     @  <label><input type="checkbox" name="ay"%s(oa['y']) />
   730    737     @  Write Unversioned%s(B('y'))</label><br />
   731    738     @  <label><input type="checkbox" name="az"%s(oa['z']) />
   732    739     @  Download Zip%s(B('z'))</label><br />
   733    740     @  <label><input type="checkbox" name="a2"%s(oa['2']) />
   734    741     @  Read Forum%s(B('2'))</label><br />
   735    742     @  <label><input type="checkbox" name="a3"%s(oa['3']) />
   736    743     @  Write Forum%s(B('3'))</label><br />
   737    744     @  <label><input type="checkbox" name="a4"%s(oa['4']) />
   738         -  @  Moderate Forum%s(B('4'))</label>
          745  +  @  WriteTrusted Forum%s(B('4'))</label><br>
          746  +  @  <label><input type="checkbox" name="a5"%s(oa['5']) />
          747  +  @  Moderate Forum%s(B('5'))</label><br>
          748  +  @  <label><input type="checkbox" name="a6"%s(oa['6']) />
          749  +  @  Supervise Forum%s(B('6'))</label>
   739    750     @ </td></tr>
   740    751     @ </table>
   741    752     @   </td>
   742    753     @ </tr>
   743    754     @ <tr>
   744    755     @   <td class="usetupEditLabel">Selected Cap.:</td>
   745    756     @   <td>
   746    757     @     <span id="usetupEditCapability">(missing JS?)</span>
          758  +  @     <a href="%R/setup_ucap_list">(key)</a>
   747    759     @   </td>
   748    760     @ </tr>
   749    761     if( !login_is_special(zLogin) ){
   750    762       @ <tr>
   751    763       @   <td align="right">Password:</td>
   752    764       if( zPw[0] ){
   753    765         /* Obscure the password for all users */