Fossil

Check-in [cc6ca4e110]
Login

Check-in [cc6ca4e110]

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: cc6ca4e110a7cfcc66ad9034389162ef0790bb7c6c8f5a1c36fb7ba5d77bccf6
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
Unified Diff Ignore Whitespace Patch
Changes to src/default.css.
902
903
904
905
906
907
908
909
910
911
912
913
914












915
916
917
918
919
920
921
  flex-direction: column;
}
div.forumClosed {
  opacity: 0.7;
}
div.forumClosed > *:first-child::before {
  content: "[CLOSED] ";
  color: red;
  opacity: 0.7;
}
/*div.forumClosed > div.forumPostBody {
  filter: blur(5px);
}*/












.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;







|





>
>
>
>
>
>
>
>
>
>
>
>







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





















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
  db_bind_int(&q, "$rid", rid);
  res = db_step(&q)==SQLITE_ROW;
  db_reset(&q);
  return res;
}

/*





















** Returns true if p, or any parent of p, has an active "closed" tag.
** Returns 0 if !p. For an edited chain of post, the tag is checked on

** the final edit in the chain, as that permits that a post can be
** locked and later unlocked. The return value is the tagxref.rowid
** value of the tagxref entry which applies the "closed" tag, or 0 if
** no active tag is found.
**
** If bCheckParents is true then p's thread parents are checked
** (recursively) for closure, else only p is checked.
*/
static int forum_post_is_closed(ForumPost *p, int bCheckParents){
  if( !p ) return 0;

  if( p->pEditTail ) p = p->pEditTail;
  if( p->iClosed || !bCheckParents ) return p->iClosed;
  else if( p->pIrt ){
    return forum_post_is_closed(p->pIrt->pEditTail
                                ? p->pIrt->pEditTail : p->pIrt,
                                bCheckParents);
  }
  return 0;
}

/*
** Given a forum post RID, this function returns true if that post has
** an active "closed" tag. If bCheckParents is true, the latest
** version of each parent post is also checked (recursively), else
** they are not. When checking parents, the first parent which is
** closed ends the search.







**
** 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 artifact to which the closure applies.
**
** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
**   ancestor forum post.
*/
static int forum_rid_is_closed(int rid, int bCheckParents){
  static Stmt qIrt = empty_Stmt_m;
  int rc = 0;

  /* TODO: this can probably be turned into a CTE, rather than a
  ** recursive call into this function, by someone with superior
  ** SQL-fu. */

  rc = rid_has_active_tag_name(rid, "closed");
  if( rc || !bCheckParents ) return rc;
  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);
  if( rid ){
    rc = forum_rid_is_closed(rid, 1);
  }
  return rc>0 ? -rc : rc;
}

/*
** UNTESTED!
**
** Closes or re-opens the given forum RID via addition of a new
** control artifact into the repository.




**
** 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 to frid if
** forum_rid_is_closed() indicates that frid 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







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
>
|
<
<
<

|
|

|
<
>
|
|
|
<
<
<






|
|
|
|
>
>
>
>
>
>
>






|


|

|

|
<
|
<
|
>
|
|
|
|
|
|
|
|
|
|
|
<
<

|



<
<

|
>
>
>
>







|
|
|
|
|







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
192
193
194
195
196

197
198

199
200
201
202
203
204
205
206
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
** - 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 */


  db_begin_transaction();

  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;
  }

  blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
  blob_appendf(&artifact,
               "T %cclosed %z%s%F\n",
               doClose ? '*' : '-', rid_to_uuid(frid),
               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);


  /* 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_unlock_checkbox(int iClosed, int fpid){








  if( iClosed && g.perm.Admin ){















    if( iClosed>0 ){
      /* Only show the "unlock" checkbox on a post which is actually
      ** closed, not on a post which inherits that state. */
      @ <label class='warning'><input type="checkbox" name="reopen" value="1">
      @ Re-open this CLOSED post? (NOT YET IMPLEMENTED)</label>
    }else{
      @ <span class='warning'>This post is CLOSED via a parent post</span>
    }
  }





}

/*
** 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







|




>


>












>


|
|















>
>















|
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
<
<
|
<
<
>
>
>
>
>







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
403


404
405
406
407
408
409
410
      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->fpid, 1);


  }
  db_finalize(&q);

  if( computeHierarchy ){
    /* Compute the hierarchical display order */
    pPost = pThread->pFirst;
    pPost->nIndent = 1;







|
>
>







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
659
660
661
662
663
664
665
666
  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 = forum_post_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" : "")\







|







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
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
    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",P("fpid"));
    return;
  }
  bPreview = P("preview")!=0;
  iClosed = forum_rid_is_closed(fpid, froot!=fpid);
  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;







|



|







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
1535
1536
1537
1538
1539
1540
1541
1542
    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">
    if( iClosed ) forumpost_error_closed();
    forum_from_line();
    forum_post_widget(zTitle, zMimetype, zContent);
  }else{
    /* Reply */
    char *zDisplayName;
    zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
    zContent = PDT("content","");







>








<







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
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
      @ <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">
    if( iClosed ) forumpost_error_closed();
    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">
    }
    forumpost_emit_unlock_checkbox(iClosed, fpid);
  }else if( !bPreview && iClosed ){
    @ <span class='warning'>This post is CLOSED</span>
  }
  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"))> \







<











<
<
<







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"))> \