Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Rework forumpost closure to always apply to the first artifact in an edit chain to enable consistent behavior across the whole chain and responses to arbitrary versions within that chain. Add rudimentary UI elements for closing/re-opening posts, but their layout needs to be revisited (noting that they need to be in a separate form from the main editor so that closing/re-opening introduces only a smalll control artifact instead of a whole forumpost artifact). |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | forumpost-locking |
| Files: | files | file ages | folders |
| SHA3-256: |
cc6ca4e110a7cfcc66ad9034389162ef |
| User & Date: | stephan 2023-02-22 04:46:20.152 |
Context
|
2023-02-22
| ||
| 06:09 | Minor CSS tweaks to closed forum posts. ... (check-in: 3078ff46ee user: stephan tags: forumpost-locking) | |
| 04:46 | Rework forumpost closure to always apply to the first artifact in an edit chain to enable consistent behavior across the whole chain and responses to arbitrary versions within that chain. Add rudimentary UI elements for closing/re-opening posts, but their layout needs to be revisited (noting that they need to be in a separate form from the main editor so that closing/re-opening introduces only a smalll control artifact instead of a whole forumpost artifact). ... (check-in: cc6ca4e110 user: stephan tags: forumpost-locking) | |
|
2023-02-21
| ||
| 10:30 | Initial (untested) code for creating the control artifact for closing and re-opening forum threads. Extend test-forumthread's tree view to show thread closure. ... (check-in: 32fc62e681 user: stephan tags: forumpost-locking) | |
Changes
Changes to src/default.css.
| ︙ | ︙ | |||
902 903 904 905 906 907 908 |
flex-direction: column;
}
div.forumClosed {
opacity: 0.7;
}
div.forumClosed > *:first-child::before {
content: "[CLOSED] ";
| | > > > > > > > > > > > > | 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 |
flex-direction: column;
}
div.forumClosed {
opacity: 0.7;
}
div.forumClosed > *:first-child::before {
content: "[CLOSED] ";
color: darkred;
opacity: 0.7;
}
/*div.forumClosed > div.forumPostBody {
filter: blur(5px);
}*/
div.forumpost-closed-warning {
margin-top: 1em;
margin-bottom: 1em;
border-style: solid;
padding: 0.25em 0.5em;
background: yellow;
color: darkred;
font-weight: bold;
}
div.forumpost-closed-warning input[type=submit] {
padding: 0.25em;
}
.forum div > form {
margin: 0.5em 0;
}
.forum-post-collapser {
/* Common style for the bottom-of-post and right-of-post
expand/collapse widgets. */
font-size: 0.8em;
|
| ︙ | ︙ |
Changes to src/forum.c.
| ︙ | ︙ | |||
78 79 80 81 82 83 84 | db_bind_int(&q, "$rid", rid); res = db_step(&q)==SQLITE_ROW; db_reset(&q); return res; } /* | > > > > > > > > > > > > > > > > > > > > > | | > | < < < | | | < > | | | < < < | | | | > > > > > > > | | | | < | < | > | | | | | | | | | | | < < | < < | > > > > | | | | | | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
db_bind_int(&q, "$rid", rid);
res = db_step(&q)==SQLITE_ROW;
db_reset(&q);
return res;
}
/*
** Given a valid forumpost.fpid value, this function returns the first
** fpid in the chain of edits for that forum post, or rid if no prior
** versions are found.
*/
static int forumpost_head_rid(int rid){
Stmt q;
int rcRid = rid;
db_prepare(&q, "SELECT fprev FROM forumpost"
" WHERE fpid=:rid AND fprev IS NOT NULL");
db_bind_int(&q, ":rid", rid);
while( SQLITE_ROW==db_step(&q) ){
rcRid = db_column_int(&q, 0);
db_reset(&q);
db_bind_int(&q, ":rid", rcRid);
}
db_finalize(&q);
return rcRid;
}
/*
** Returns true if p, or any parent of p, has a non-zero iClosed
** value. Returns 0 if !p. For an edited chain of post, the tag is
** checked on the pEditHead entry, to simplify subsequent unlocking of
** the post.
**
** If bCheckIrt is true then p's thread in-response-to parents are
** checked (recursively) for closure, else only p is checked.
*/
static int forumpost_is_closed(ForumPost *p, int bCheckIrt){
while(p){
if( p->pEditHead ) p = p->pEditHead;
if( p->iClosed || !bCheckIrt ) return p->iClosed;
p = p->pIrt;
}
return 0;
}
/*
** Given a forum post RID, this function returns true if that post has
** (or inherits) an active "closed" tag. If bCheckIrt is true then
** the post to which the given post responds is also checked
** (recursively), else they are not. When checking in-response-to
** posts, the first one which is closed ends the search.
**
** Note that this function checks _exactly_ the given rid, whereas
** forum post closure/re-opening is always applied to the head of an
** edit chain so that we get consistent implied locking beheavior for
** later versions and responses to arbitrary versions in the
** chain. Even so, the "closed" tag is applied as a propagating tag
** so will apply to all edits in a given chain.
**
** The return value is one of:
**
** - 0 if no "closed" tag is found.
**
** - The tagxref.rowid of the tagxref entry for the closure if rid is
** the forum post to which the closure applies.
**
** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
** IRT forum post.
*/
static int forum_rid_is_closed(int rid, int bCheckIrt){
static Stmt qIrt = empty_Stmt_m;
int rc = 0, i = 0;
/* TODO: this can probably be turned into a CTE by someone with
** superior SQL-fu. */
for( ; rid; i++ ){
rc = rid_has_active_tag_name(rid, "closed");
if( rc || !bCheckIrt ) break;
else if( !qIrt.pStmt ) {
db_static_prepare(&qIrt,
"SELECT firt FROM forumpost "
"WHERE fpid=$fpid ORDER BY fmtime DESC"
);
}
db_bind_int(&qIrt, "$fpid", rid);
rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
db_reset(&qIrt);
}
return i ? -rc : rc;
}
/*
** Closes or re-opens the given forum RID via addition of a new
** control artifact into the repository. In order to provide
** consistent behavior for implied closing of responses and later
** versions, it always acts on the first version of the given forum
** post, walking the forumpost.fprev values to find the head of the
** chain.
**
** If doClose is true then a propagating "closed" tag is added, except
** as noted below, with the given optional zReason string as the tag's
** value. If doClose is false then any active "closed" tag on frid is
** cancelled, except as noted below. zReason is ignored if doClose is
** false or if zReason is NULL or starts with a NUL byte.
**
** This function only adds a "closed" tag if forum_rid_is_closed()
** indicates that frid's head is not closed. If a parent post is
** already closed, no tag is added. Similarly, it will only remove a
** "closed" tag from a post which has its own "closed" tag, and will
** not remove an inherited one from a parent post.
**
** If doClose is true and frid is closed (directly or inherited), this
** is a no-op. Likewise, if doClose is false and frid itself is not
** closed (not accounting for an inherited closed tag), this is a
** no-op.
**
** Returns true if it actually creates a new tag, else false. Fails
|
| ︙ | ︙ | |||
185 186 187 188 189 190 191 | ** - This routine assumes that frid is not private or pending ** moderation. ** ** - Closure of a forum post requires a propagating "closed" tag to ** account for how edits of posts are handled. This differs from ** closure of a branch, where a non-propagating tag is used. */ | | > > > | | > > | > > > > > > > > | > > > > > > > > > > > > > > > | | | | < < | < < > > > > > | 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
** - This routine assumes that frid is not private or pending
** moderation.
**
** - Closure of a forum post requires a propagating "closed" tag to
** account for how edits of posts are handled. This differs from
** closure of a branch, where a non-propagating tag is used.
*/
static int forumpost_close(int frid, int doClose, const char *zReason){
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
Blob cksum = BLOB_INITIALIZER; /* Z-card */
int iClosed; /* true if frid is closed */
int trid; /* RID of new control artifact */
char *zUuid; /* UUID of head version of post */
db_begin_transaction();
frid = forumpost_head_rid(frid);
iClosed = forum_rid_is_closed(frid, 1);
if( (iClosed && doClose
/* Already closed, noting that in the case of (iClosed<0), it's
** actually a parent which is closed. */)
|| (iClosed<=0 && !doClose
/* This entry is not closed, but a parent post may be. */) ){
db_end_transaction(0);
return 0;
}
if( doClose==0 || (zReason && !zReason[0]) ){
zReason = 0;
}
zUuid = rid_to_uuid(frid);
blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
blob_appendf(&artifact,
"T %cclosed %s%s%F\n",
doClose ? '*' : '-', zUuid,
zReason ? " " : "", zReason ? zReason : "");
blob_appendf(&artifact, "U %F\n", login_name());
md5sum_blob(&artifact, &cksum);
blob_appendf(&artifact, "Z %b\n", &cksum);
blob_reset(&cksum);
trid = content_put_ex(&artifact, 0, 0, 0, 0);
if( trid==0 ){
fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
}
if( manifest_crosslink(trid, &artifact,
MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
fossil_fatal("%s", g.zErrMsg);
}
assert( blob_is_reset(&artifact) );
db_add_unsent(trid);
admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid);
fossil_free(zUuid);
/* Potential TODO: if (iClosed>0) then we could find the initial tag
** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
** size of these artifacts, however, that would save little space,
** if any. */
db_end_transaction(0);
return 1;
}
/*
** If iClosed is true and the current user has admin privileges, this
** renders either a checkbox to unlock forum post fpid (if iClosed>0)
** or a SPAN.warning element that the given post inherits the CLOSED
** status from a parent post (if iClosed<0). If neither of the initial
** conditions is true, this is a no-op.
*/
static void forumpost_emit_closed_state(int fpid, int iClosed){
const char *zCommon =
"Only admins may edit or respond to closed posts.";
int iHead = forumpost_head_rid(fpid);
/*@ forumpost_emit_closed_state(%d(fpid), %d(iClosed))<br/>*/
if( iHead != fpid ){
iClosed = forum_rid_is_closed(iHead, 1);
/*@ forumpost_emit_closed_state() %d(iHead), %d(iClosed)*/
}
if( iClosed<0 ){
@ <div class="warning forumpost-closed-warning">\
@ This post is CLOSED via a parent post. %s(zCommon)\
@ </div>
return;
}
else if( iClosed==0 ){
if( g.perm.Admin==0 ) return;
@ <div class="warning forumpost-closed-warning">
@ <form method="post" action="%R/forumpost_close">
@ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
@ <input type="submit" value="CLOSE this post and its responses" />
@ %s(zCommon)
@ </form></div>
return;
}
assert( iClosed>0 );
/* Only show the "unlock" checkbox on a post which is actually
** closed, not on a post which inherits that state. */
@ <div class="warning forumpost-closed-warning">\
@ This post is CLOSED. %s(zCommon)
@ <form method="post" action="%R/forumpost_reopen">
@ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
@ <input type="submit" value="Re-open this post and its responses" />
@ </form>
@ </div>
}
/*
** Emits a warning that the current forum post is CLOSED and can only
** be edited or responded to by an administrator. */
static void forumpost_error_closed(void){
@ <div class='error'>This (sub)thread is CLOSED and can only be
|
| ︙ | ︙ | |||
396 397 398 399 400 401 402 |
pPost->pEditPrev = p;
pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
for(; p; p=p->pEditPrev ){
p->nEdit = pPost->nEdit;
p->pEditTail = pPost;
}
}
| | > > | 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 |
pPost->pEditPrev = p;
pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
for(; p; p=p->pEditPrev ){
p->nEdit = pPost->nEdit;
p->pEditTail = pPost;
}
}
pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
? pPost->pEditHead->fpid
: pPost->fpid, 1);
}
db_finalize(&q);
if( computeHierarchy ){
/* Compute the hierarchical display order */
pPost = pThread->pFirst;
pPost->nIndent = 1;
|
| ︙ | ︙ | |||
652 653 654 655 656 657 658 | int iIndent; /* Indent level */ int iClosed; /* True if (sub)thread is closed */ const char *zMimetype;/* Formatting MIME type */ /* Get the manifest for the post. Abort if not found (e.g. shunned). */ pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); if( !pManifest ) return; | | | 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 |
int iIndent; /* Indent level */
int iClosed; /* True if (sub)thread is closed */
const char *zMimetype;/* Formatting MIME type */
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
if( !pManifest ) return;
iClosed = forumpost_is_closed(p, 1);
/* When not in raw mode, create the border around the post. */
if( !bRaw ){
/* Open the <div> enclosing the post. Set the class string to mark the post
** as selected and/or obsolete. */
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
@ <div id='forum%d(p->fpid)' class='forumTime\
@ %s(bSelect ? " forumSel" : "")\
|
| ︙ | ︙ | |||
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 |
@ maxlength="125"><br>
}
@ %z(href("%R/markup_help"))Markup style</a>:
mimetype_option_menu(zMimetype, "mimetype");
@ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
}
/*
** WEBPAGE: forumnew
** WEBPAGE: forumedit
**
** Start a new thread on the forum or reply to an existing thread.
** But first prompt to see if the user would like to log in.
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 |
@ maxlength="125"><br>
}
@ %z(href("%R/markup_help"))Markup style</a>:
mimetype_option_menu(zMimetype, "mimetype");
@ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
}
/*
** WEBPAGE: forumpost_close hidden
** WEBPAGE: forumpost_reopen hidden
**
** fpid=X Hash of the post to be edited. REQUIRED
** reason=X Optional reason for closure.
**
** Closes or re-opens the given forum post, within the bounds of the
** API for forumpost_close(). After (perhaps) modifying the "closed"
** status of the given thread, it redirects to that post's thread
** view. Requires admin privileges.
*/
void forum_page_close(void){
const char *zFpid = PD("fpid","");
const char *zReason = 0;
int fClose;
int fpid;
login_check_credentials();
if( !g.perm.Admin ){
login_needed(g.anon.Admin);
return;
}
fpid = symbolic_name_to_rid(zFpid, "f");
if( fpid<=0 ){
webpage_error("Missing or invalid fpid query parameter");
}
fClose = sqlite3_strglob("*_close*", g.zPath)==0;
if( fClose ) zReason = PD("reason",0);
if( forumpost_close(fpid, fClose, zReason)!=0 ){
admin_log("%s forum post %S", fClose ? "Close" : "Re-open", zFpid);
}
cgi_redirectf("%R/forumpost/%S",zFpid);
return;
}
/*
** WEBPAGE: forumnew
** WEBPAGE: forumedit
**
** Start a new thread on the forum or reply to an existing thread.
** But first prompt to see if the user would like to log in.
|
| ︙ | ︙ | |||
1428 1429 1430 1431 1432 1433 1434 |
webpage_error("Missing or invalid fpid query parameter");
}
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
}
if( P("cancel") ){
| | | | 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 |
webpage_error("Missing or invalid fpid query parameter");
}
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
}
if( P("cancel") ){
cgi_redirectf("%R/forumpost/%S",zFpid);
return;
}
bPreview = P("preview")!=0;
iClosed = forum_rid_is_closed(fpid, 1);
isCsrfSafe = cgi_csrf_safe(1);
bPrivate = content_is_private(fpid);
bSameUser = login_is_individual()
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
if( g.perm.ModForum && P("approve") ){
const char *zUserToTrust;
|
| ︙ | ︙ | |||
1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 |
zMimetype = "text/x-fossil-wiki";
zContent = "";
if( pPost->zThreadTitle ) zTitle = "";
style_header("Delete %s", zTitle ? "Post" : "Reply");
@ <h1>Original Post:</h1>
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
"forumEdit", 1);
@ <h1>Change Into:</h1>
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="nullout" value="1">
@ <input type="hidden" name="mimetype" value="%h(zMimetype)">
@ <input type="hidden" name="content" value="%h(zContent)">
| > | 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 |
zMimetype = "text/x-fossil-wiki";
zContent = "";
if( pPost->zThreadTitle ) zTitle = "";
style_header("Delete %s", zTitle ? "Post" : "Reply");
@ <h1>Original Post:</h1>
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
"forumEdit", 1);
forumpost_emit_closed_state(fpid, iClosed);
@ <h1>Change Into:</h1>
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="nullout" value="1">
@ <input type="hidden" name="mimetype" value="%h(zMimetype)">
@ <input type="hidden" name="content" value="%h(zContent)">
|
| ︙ | ︙ | |||
1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 |
if( zTitle==0 && pPost->zThreadTitle!=0 ){
zTitle = fossil_strdup(pPost->zThreadTitle);
}
style_header("Edit %s", zTitle ? "Post" : "Reply");
@ <h2>Original Post:</h2>
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
"forumEdit", 1);
if( bPreview ){
@ <h2>Preview of Edited Post:</h2>
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
}
@ <h2>Revised Message:</h2>
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="edit" value="1">
| > < | 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 |
if( zTitle==0 && pPost->zThreadTitle!=0 ){
zTitle = fossil_strdup(pPost->zThreadTitle);
}
style_header("Edit %s", zTitle ? "Post" : "Reply");
@ <h2>Original Post:</h2>
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
"forumEdit", 1);
forumpost_emit_closed_state(fpid, iClosed);
if( bPreview ){
@ <h2>Preview of Edited Post:</h2>
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
}
@ <h2>Revised Message:</h2>
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="edit" value="1">
forum_from_line();
forum_post_widget(zTitle, zMimetype, zContent);
}else{
/* Reply */
char *zDisplayName;
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
zContent = PDT("content","");
|
| ︙ | ︙ | |||
1558 1559 1560 1561 1562 1563 1564 |
@ <h2>Preview:</h2>
forum_render(0, zMimetype,zContent, "forumEdit", 1);
}
@ <h2>Enter Reply:</h2>
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="reply" value="1">
| < < < < | 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 |
@ <h2>Preview:</h2>
forum_render(0, zMimetype,zContent, "forumEdit", 1);
}
@ <h2>Enter Reply:</h2>
@ <form action="%R/forume2" method="POST">
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
@ <input type="hidden" name="reply" value="1">
forum_from_line();
forum_post_widget(0, zMimetype, zContent);
}
if( !isDelete ){
@ <input type="submit" name="preview" value="Preview">
}
@ <input type="submit" name="cancel" value="Cancel">
if( (bPreview && !whitespace_only(zContent)) || isDelete ){
if( !iClosed || g.perm.Admin ) {
@ <input type="submit" name="submit" value="Submit">
}
}
if( g.perm.Debug ){
/* For the test-forumnew page add these extra debugging controls */
@ <div class="debug">
@ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
@ Dry run</label>
@ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
|
| ︙ | ︙ |