Fossil SCM

Update Blitz skin style to treat buttons consistently to input[type=button].

stephan 2026-05-26 13:30 UTC forum-statuses
Commit 25b18d427cd3de8be97788d913360f67ff1b4309196e73ffb21700ab0709ab92
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -288,11 +288,13 @@
288288
*/
289289
290290
button,
291291
html input[type="button"], /* 1 */
292292
input[type="reset"],
293
-input[type="submit"] {
293
+input[type="submit"],
294
+input[type="button"].submit,
295
+button.submit{
294296
-webkit-appearance: button; /* 2 */
295297
cursor: pointer; /* 3 */
296298
}
297299
298300
/**
@@ -517,11 +519,13 @@
517519
––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */
518520
.button,
519521
button,
520522
input[type="button"],
521523
input[type="reset"],
522
-input[type="submit"] {
524
+input[type="submit"],
525
+input[type="button"].submit,
526
+button.submit{
523527
display: inline-block;
524528
height: 3.3rem;
525529
padding: 0 2.2rem;
526530
color: #555 !important;
527531
text-align: center;
@@ -551,24 +555,32 @@
551555
background-color: #eee;
552556
border-color: #aaa;
553557
outline: 0;
554558
}
555559
556
-input[type="submit"] {
560
+input[type="submit"],
561
+input[type="button"].submit,
562
+button.submit{
557563
color: white !important;
558564
background-color: #446979;
559565
border-color: #446979;
560566
}
561567
562568
input[type="submit"]:hover,
563
-input[type="submit"]:focus {
569
+input[type="submit"]:focus,
570
+input[type="button"].submit:hover,
571
+input[type="button"].submit:focus,
572
+button.submit:hover,
573
+button.submit:focus{
564574
color: white !important;
565575
background-color: #648898;
566576
border-color: #648898;
567577
}
568578
569
-input[type="submit"]:disabled {
579
+input[type="submit"]:disabled,
580
+input[type="button"].submit:disabled,
581
+button.submit:disabled{
570582
color: rgb(128,128,128);
571583
background-color: rgb(153,153,153);
572584
}
573585
574586
575587
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -288,11 +288,13 @@
288 */
289
290 button,
291 html input[type="button"], /* 1 */
292 input[type="reset"],
293 input[type="submit"] {
 
 
294 -webkit-appearance: button; /* 2 */
295 cursor: pointer; /* 3 */
296 }
297
298 /**
@@ -517,11 +519,13 @@
517 ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */
518 .button,
519 button,
520 input[type="button"],
521 input[type="reset"],
522 input[type="submit"] {
 
 
523 display: inline-block;
524 height: 3.3rem;
525 padding: 0 2.2rem;
526 color: #555 !important;
527 text-align: center;
@@ -551,24 +555,32 @@
551 background-color: #eee;
552 border-color: #aaa;
553 outline: 0;
554 }
555
556 input[type="submit"] {
 
 
557 color: white !important;
558 background-color: #446979;
559 border-color: #446979;
560 }
561
562 input[type="submit"]:hover,
563 input[type="submit"]:focus {
 
 
 
 
564 color: white !important;
565 background-color: #648898;
566 border-color: #648898;
567 }
568
569 input[type="submit"]:disabled {
 
 
570 color: rgb(128,128,128);
571 background-color: rgb(153,153,153);
572 }
573
574
575
--- skins/blitz/css.txt
+++ skins/blitz/css.txt
@@ -288,11 +288,13 @@
288 */
289
290 button,
291 html input[type="button"], /* 1 */
292 input[type="reset"],
293 input[type="submit"],
294 input[type="button"].submit,
295 button.submit{
296 -webkit-appearance: button; /* 2 */
297 cursor: pointer; /* 3 */
298 }
299
300 /**
@@ -517,11 +519,13 @@
519 ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */
520 .button,
521 button,
522 input[type="button"],
523 input[type="reset"],
524 input[type="submit"],
525 input[type="button"].submit,
526 button.submit{
527 display: inline-block;
528 height: 3.3rem;
529 padding: 0 2.2rem;
530 color: #555 !important;
531 text-align: center;
@@ -551,24 +555,32 @@
555 background-color: #eee;
556 border-color: #aaa;
557 outline: 0;
558 }
559
560 input[type="submit"],
561 input[type="button"].submit,
562 button.submit{
563 color: white !important;
564 background-color: #446979;
565 border-color: #446979;
566 }
567
568 input[type="submit"]:hover,
569 input[type="submit"]:focus,
570 input[type="button"].submit:hover,
571 input[type="button"].submit:focus,
572 button.submit:hover,
573 button.submit:focus{
574 color: white !important;
575 background-color: #648898;
576 border-color: #648898;
577 }
578
579 input[type="submit"]:disabled,
580 input[type="button"].submit:disabled,
581 button.submit:disabled{
582 color: rgb(128,128,128);
583 background-color: rgb(153,153,153);
584 }
585
586
587
--- src/default.css
+++ src/default.css
@@ -1118,10 +1118,14 @@
11181118
11191119
body.forum div.forumPosts table tr.pinned > td.subject:before {
11201120
content: "📌 "/*this space works around an unsightly FF quirk*/;
11211121
font-size: 120%;
11221122
}
1123
+
1124
+body.forum span.forum-status-selection {
1125
+ white-space: nowrap;
1126
+}
11231127
11241128
body.cpage-setup_forum > .content table {
11251129
margin-bottom: 1em;
11261130
}
11271131
body.cpage-setup_forum > .content table.bordered {
11281132
--- src/default.css
+++ src/default.css
@@ -1118,10 +1118,14 @@
1118
1119 body.forum div.forumPosts table tr.pinned > td.subject:before {
1120 content: "📌 "/*this space works around an unsightly FF quirk*/;
1121 font-size: 120%;
1122 }
 
 
 
 
1123
1124 body.cpage-setup_forum > .content table {
1125 margin-bottom: 1em;
1126 }
1127 body.cpage-setup_forum > .content table.bordered {
1128
--- src/default.css
+++ src/default.css
@@ -1118,10 +1118,14 @@
1118
1119 body.forum div.forumPosts table tr.pinned > td.subject:before {
1120 content: "📌 "/*this space works around an unsightly FF quirk*/;
1121 font-size: 120%;
1122 }
1123
1124 body.forum span.forum-status-selection {
1125 white-space: nowrap;
1126 }
1127
1128 body.cpage-setup_forum > .content table {
1129 margin-bottom: 1em;
1130 }
1131 body.cpage-setup_forum > .content table.bordered {
1132
+180 -57
--- src/forum.c
+++ src/forum.c
@@ -255,14 +255,24 @@
255255
** matches the event.(euser,user) field for a formpost entry with the
256256
** matching RID. Returns false if no match is found. If zUserName is
257257
** 0 then login_name() is used.
258258
*/
259259
int forumpost_is_owner(int rid, const char *zUserName){
260
- return db_int(0, "SELECT 1 FROM event "
261
- "WHERE type='f' AND objid=%d "
262
- "AND coalesce(euser,user)=%Q",
263
- rid, zUserName ? zUserName : login_name());
260
+ static Stmt q;
261
+ int rc;
262
+ if( !q.pStmt ){
263
+ db_static_prepare(
264
+ &q, "SELECT 1 FROM event"
265
+ " WHERE type='f' AND objid=$rid"
266
+ " AND coalesce(euser,user)=$user"
267
+ );
268
+ }
269
+ db_bind_int(&q, "$rid", rid);
270
+ db_bind_text(&q, "$user", zUserName ? zUserName : login_name());
271
+ rc = SQLITE_ROW==db_step(&q);
272
+ db_reset(&q);
273
+ return rc;
264274
}
265275
266276
/*
267277
** Returns true if p, or any parent of p, has a non-zero iClosed
268278
** value. Returns 0 if !p. For an edited chain of post, the tag is
@@ -397,25 +407,27 @@
397407
** provide consistent behavior, it always acts on the first version of
398408
** the given forum post, walking the forumpost.fprev values to find
399409
** the head of the chain.
400410
**
401411
** If addTag is true then a propagating tag is added, except as noted
402
-** below, with the given optional zReason string as the tag's
412
+** below, with the given optional zValue string as the tag's
403413
** value. If addTag is false then any matching active tag on frid is
404
-** cancelled, except as noted below. zReason is ignored if it is NULL
414
+** cancelled, except as noted below. zValue is ignored if it is NULL
405415
** or starts with a NUL byte, or if addTag is false.
406416
**
407417
** This function only adds a tag if forum_rid_is_tagged() indicates
408418
** that frid's head is not tagged. If a parent post is already tagged,
409419
** no tag is added. Similarly, it will only remove a tag from a post
410420
** which has its own tag, and will not remove an inherited one from a
411421
** parent post.
412422
**
413
-** If addTag is true and frid is already tagged (directly or
414
-** inherited), this is a no-op. Likewise, if addTag is false and frid
415
-** itself is not tagged (not accounting for an inherited closed tag),
416
-** this is a no-op.
423
+** If addTag is true and frid is already tagged, this is a
424
+** no-op. Likewise, if addTag is false and frid is not tagged
425
+** (not accounting for an inherited closed tag), this is a no-op.
426
+**
427
+** If bCheckIrt is true then the forum post IRT hierarchy is searched
428
+** for the tag, otherwise only the given RID is checked.
417429
**
418430
** Returns true if it actually creates a new tag, else false. Fails
419431
** fatally on error.
420432
**
421433
** If it returns true then state from previously-loaded posts may be
@@ -438,57 +450,62 @@
438450
** - The applied tag is propagating so so that "closed" tags can
439451
** account for how edits of posts are handled. This differs from
440452
** closure of a branch, where a non-propagating tag is used.
441453
*/
442454
static int forumpost_tag(int frid, const char *zTagName, int addTag,
443
- const char *zReason){
455
+ const char *zValue){
444456
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
445457
Blob cksum = BLOB_INITIALIZER; /* Z-card */
446458
int iTagged; /* true if frid is already tagged */
447459
int trid; /* RID of new control artifact */
448460
char *zUuid; /* UUID of head version of post */
449461
450462
db_begin_transaction();
451463
frid = forumpost_head_rid(frid);
452
- iTagged = forum_rid_is_tagged(frid, zTagName, 1);
453
- if( (iTagged && addTag
454
- /* Already tagged, noting that in the case of (addTag<0) it may
455
- ** actually be a parent which is tagged. */)
456
- || (iTagged<=0 && !addTag
457
- /* This entry is not tagged, but a parent post may be. */) ){
464
+ iTagged = forum_rid_is_tagged(frid, zTagName, 0);
465
+ if( !addTag && !iTagged ){
466
+ /* Nothing to do. We never tag an IRT-inherited post via this
467
+ ** function. */
458468
db_end_transaction(0);
459469
return 0;
460470
}
461
- if( addTag==0 || (zReason && !zReason[0]) ){
462
- zReason = 0;
471
+ if( !addTag || (zValue && !zValue[0]) ){
472
+ zValue = 0;
473
+ }
474
+ if( addTag && iTagged ){
475
+ char *zOld = 0;
476
+ int cmp;
477
+ rid_has_tag2(iTagged, zTagName, &zOld);
478
+ cmp = fossil_strcmp(zOld, zValue);
479
+ fossil_free(zOld);
480
+ if( 0==cmp ){
481
+ /* Same value - leave it as is. */
482
+ db_end_transaction(0);
483
+ return 0;
484
+ }
463485
}
464486
zUuid = rid_to_uuid(frid);
465487
blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
466488
blob_appendf(&artifact, "T %c%s %s%s%F\n",
467489
addTag ? '*' : '-', zTagName,
468
- zUuid, zReason ? " " : "", zReason ? zReason : "");
490
+ zUuid, zValue ? " " : "", zValue ? zValue : "");
469491
blob_appendf(&artifact, "U %F\n", login_name());
470492
md5sum_blob(&artifact, &cksum);
471493
blob_appendf(&artifact, "Z %b\n", &cksum);
472494
blob_reset(&cksum);
473495
trid = content_put_ex(&artifact, 0, 0, 0, 0);
474496
if( trid==0 ){
475497
fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
476498
}
477
- if( manifest_crosslink(trid, &artifact,
478
- MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
499
+ if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){
479500
fossil_fatal("%s", g.zErrMsg);
480501
}
481502
assert( blob_is_reset(&artifact) );
482503
db_add_unsent(trid);
483504
admin_log("Tag forum post %S with %c%s",
484505
zUuid, addTag ? '*' : '-', zTagName);
485506
fossil_free(zUuid);
486
- /* Potential TODO: if (iClosed>0) then we could find the initial tag
487
- ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
488
- ** size of these artifacts, however, that would save little space,
489
- ** if any. */
490507
db_end_transaction(0);
491508
return 1;
492509
}
493510
494511
/*
@@ -853,10 +870,72 @@
853870
}
854871
@ </table>
855872
db_finalize(&q);
856873
style_finish_page();
857874
}
875
+
876
+/*
877
+** Returns true if the current user is authorized to set forum post
878
+** fpid's status.
879
+*/
880
+static int forum_may_set_status(int fpid){
881
+ return g.perm.Admin
882
+ || g.perm.ModForum
883
+ || (login_is_individual()
884
+ && forumpost_is_owner(fpid, 0));
885
+}
886
+
887
+/*
888
+** If the current user is authorized to set fp's status then this
889
+** renders a mini-form for setting the status then redirecting back to
890
+** the post. Else it may emit a status label or no output.
891
+*/
892
+static void forum_render_status_selection( const ForumPost *fp ){
893
+ const ForumStatusList * const fss = forum_statuses();
894
+ if( fss->n>1 ){
895
+ const ForumPost * pHead = fp->pEditHead ? fp->pEditHead : fp;
896
+ int i;
897
+ char * zCurrent = 0;
898
+ const ForumStatus * sCurrent = 0;
899
+ rid_has_tag2(pHead->fpid, "status", &zCurrent);
900
+ for( i = 0; i < fss->n; ++i ){
901
+ const ForumStatus * const fs = &fss->aStatus[i];
902
+ if( 0==fossil_strcmp(zCurrent, fs->zValue) ){
903
+ sCurrent = fs;
904
+ break;
905
+ }
906
+ }
907
+ if( !sCurrent ) sCurrent = &fss->aStatus[0];
908
+ assert( sCurrent );
909
+ @ <span class='forum-status-selection'>
910
+ if( forum_may_set_status(fp->fpid)
911
+ /* FIXME: only do this if fp is the currently-selected post */ ){
912
+ @ <form method="post" action='%R/forumpost_status'>
913
+ login_insert_csrf_secret();
914
+ @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
915
+ @ <select name='status' data-fpid='%s(fp->zUuid)>'\
916
+ @ data-initial-value='%h(zCurrent ? zCurrent : "")'>
917
+ for( i = 0; i < fss->n; ++i ){
918
+ const ForumStatus * const fs = &fss->aStatus[i];
919
+ @ <option value='%h(fs->zValue)'\
920
+ @ %s(sCurrent==fs ? " selected" : "")>\
921
+ @ %h(fs->zLabel)</option>
922
+ }
923
+ @ </select>
924
+ @ <input type='button' class='submit action-status' disabled
925
+ @ value='Change' />
926
+ /* ^^^ This must be <input>, not <button>, or else tapping it
927
+ ** will unconditionally submit. */
928
+ @ </form>
929
+ /* Form is activated in fossil.page.forumpost.js */
930
+ }else{
931
+ @ Status: %h(sCurrent->zLabel);
932
+ }
933
+ @ </span>
934
+ fossil_free(zCurrent);
935
+ }
936
+}
858937
859938
/*
860939
** Render a forum post for display
861940
*/
862941
void forum_render(
@@ -1129,11 +1208,11 @@
11291208
/* Provide a link to the raw source code. */
11301209
if( !bUnf ){
11311210
@ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a>
11321211
}
11331212
@ </h3>
1134
- }
1213
+ }/*!bRaw*/
11351214
11361215
/* Check if this post is approved, also if it's by the current user. */
11371216
bPrivate = content_is_private(p->fpid);
11381217
bSameUser = login_is_individual()
11391218
&& fossil_strcmp(pManifest->zUser, g.zLogin)==0;
@@ -1193,25 +1272,25 @@
11931272
login_insert_csrf_secret();
11941273
@ </form>
11951274
11961275
if( bSelect ){
11971276
const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
1277
+ const int bIsOwner = forumpost_is_owner(p/*not pHead*/->fpid, 0);
11981278
if( forumpost_may_close() && iClosed>=0 ){
11991279
@ <form method="post" \
12001280
@ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
12011281
login_insert_csrf_secret();
12021282
@ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
12031283
if( moderation_pending(p->fpid)==0 ){
12041284
@ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1205
- @ class='hidden %s(iClosed ? "action-reopen" : "action-close")'/>
1285
+ @ class='submit hidden \
1286
+ @ %s(iClosed ? "action-reopen" : "action-close")'/>
12061287
/* ^^^ activated by fossil.page.forumpost.js */
12071288
}
12081289
@ </form>
12091290
}
1210
- if( g.perm.Admin ||
1211
- (login_is_individual()
1212
- && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){
1291
+ if( g.perm.Admin || (login_is_individual() && bIsOwner) ){
12131292
/* When an admin edits someone else's post, the admin
12141293
** effectively takes over ownership of it (and we currently
12151294
** have no way of passing it back). Because of this, we
12161295
** check the ownership of `p` instead of `pHead`. */
12171296
@ <form method="post" action="%R/attachadd">\
@@ -1219,26 +1298,33 @@
12191298
@ <input type="submit" value="Attach...">
12201299
login_insert_csrf_secret();
12211300
moderation_pending_www(p->fpid);
12221301
@ </form>
12231302
}
1224
- if( !p->pIrt && g.perm.Setup ){
1225
- const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0);
1226
- @ <form method="post" \
1227
- @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'>
1228
- login_insert_csrf_secret();
1229
- @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1230
- @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \
1231
- @ class='hidden %s(isPinned ? "action-unpin" : "action-pin")'/>
1232
- /* ^^^ activated by fossil.page.forumpost.js */
1233
- @ </form>
1303
+ if( !p->pIrt ){
1304
+ /* Root node only... */
1305
+ if( g.perm.Setup ){
1306
+ const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0);
1307
+ @ <form method="post" \
1308
+ @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'>
1309
+ login_insert_csrf_secret();
1310
+ @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1311
+ @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \
1312
+ @ class='submit hidden \
1313
+ @ %s(isPinned ? "action-unpin" : "action-pin")'/>
1314
+ /* ^^^ activated by fossil.page.forumpost.js */
1315
+ @ </form>
1316
+ }
12341317
}
12351318
}
12361319
@ </div>
12371320
}
1321
+ if( !p->pIrt && (flags & FDISPLAY_SELECTED)){
1322
+ forum_render_status_selection(p);
1323
+ }
12381324
@ </div>
1239
- }
1325
+ }/*!bRaw*/
12401326
12411327
/* Clean up. */
12421328
manifest_destroy(pManifest);
12431329
}
12441330
@@ -1730,28 +1816,41 @@
17301816
mimetype_option_menu(zMimetype, "mimetype");
17311817
@ <div class="forum-editor-widget">
17321818
@ <textarea aria-label="Content:" name="content" class="wikiedit" \
17331819
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div>
17341820
}
1821
+
1822
+/*
1823
+** If PD("fpid") refers to a forum post, its rid is returned, else
1824
+** this function emits an error does not does return.
1825
+*/
1826
+static int forum_validate_fpid_param(void){
1827
+ const char *zFpid = PD("fpid","");
1828
+ int fpid = symbolic_name_to_rid(zFpid, "f");
1829
+ if( fpid<=0 ){
1830
+ webpage_error("Missing or invalid fpid parameter.");
1831
+ }
1832
+ return fpid;
1833
+}
17351834
17361835
/*
17371836
** Internal helper for /forumpost_XYZ internal pages which tag/untag
17381837
** posts.
17391838
*/
17401839
static void forumpost_action_helper(const char *zTag, const char *zVal,
1741
- int addTag){
1742
- const char *zFpid = PD("fpid","");
1743
- int fpid;
1744
-
1745
- cgi_csrf_verify();
1746
- fpid = symbolic_name_to_rid(zFpid, "f");
1747
- if( fpid<=0 ){
1748
- webpage_error("Missing or invalid fpid query parameter");
1749
- }
1750
- forumpost_tag(fpid, zTag, addTag, zVal);
1751
- cgi_redirectf("%R/forumpost/%S",zFpid);
1752
- return;
1840
+ int addTag, int validFpid){
1841
+ if( !cgi_csrf_safe(2) ){
1842
+ webpage_error("CSRF validation failed");
1843
+ }else{
1844
+ const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1845
+ forumpost_tag(fpid, zTag, addTag, zVal);
1846
+#if 0
1847
+ @ DEBUG frid=%d(fpid) addTag=%d(addTag) %h(zTag)=%h(zVal ? zVal : "NULL")
1848
+#else
1849
+ cgi_redirectf("%R/forumpost/%S",P("fpid"));
1850
+#endif
1851
+ }
17531852
}
17541853
17551854
/*
17561855
** WEBPAGE: forumpost_close hidden
17571856
** WEBPAGE: forumpost_reopen hidden
@@ -1769,11 +1868,11 @@
17691868
if( forumpost_may_close()==0 ){
17701869
login_needed(g.anon.Admin);
17711870
}else{
17721871
const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0;
17731872
char const *zReason = bIsAdd ? 0 : PD("reason", 0);
1774
- forumpost_action_helper("closed", zReason, bIsAdd);
1873
+ forumpost_action_helper("closed", zReason, bIsAdd, 0);
17751874
}
17761875
}
17771876
17781877
/*
17791878
** WEBPAGE: forumpost_pin hidden
@@ -1790,11 +1889,35 @@
17901889
login_check_credentials();
17911890
if( !g.perm.Setup ){
17921891
login_needed(g.anon.Setup);
17931892
}else{
17941893
const int bIsAdd = sqlite3_strglob("*_pin*", g.zPath)==0;
1795
- forumpost_action_helper("pinned", 0, bIsAdd);
1894
+ forumpost_action_helper("pinned", 0, bIsAdd, 0);
1895
+ }
1896
+}
1897
+
1898
+/*
1899
+** WEBPAGE: forumpost_status hidden
1900
+**
1901
+** fpid=X Hash of the post to be edited. REQUIRED
1902
+** status=Y New status value. REQUIRED
1903
+**
1904
+** Updates the current status=Y tag on the first version of
1905
+** the forum post X. Requires forum_may_set_status() permissions.
1906
+*/
1907
+void forum_page_status(void){
1908
+ int fpid;
1909
+ login_check_credentials();
1910
+ fpid = forum_validate_fpid_param();
1911
+ if(forum_may_set_status(fpid)){
1912
+ const char *zStatus = PD("status",0);
1913
+ if( !zStatus || !zStatus[0] ){
1914
+ webpage_error("Missing required status.");
1915
+ }
1916
+ forumpost_action_helper("status", zStatus, 1, fpid);
1917
+ }else{
1918
+ webpage_error("You lack permissions to change this post's status.");
17961919
}
17971920
}
17981921
17991922
/*
18001923
** WEBPAGE: forumnew
@@ -2466,11 +2589,11 @@
24662589
char *zDuration = human_readable_age(db_column_double(&q,1));
24672590
@ %d(nMsg) posts spanning %h(zDuration)\
24682591
fossil_free(zDuration);
24692592
}
24702593
@ </td>\
2471
- if( bHasStatus ){
2594
+ if( zStatus ){
24722595
@ <td>%h(zStatus)</td>\
24732596
}
24742597
@</tr>
24752598
fossil_free(zAge);
24762599
}
24772600
--- src/forum.c
+++ src/forum.c
@@ -255,14 +255,24 @@
255 ** matches the event.(euser,user) field for a formpost entry with the
256 ** matching RID. Returns false if no match is found. If zUserName is
257 ** 0 then login_name() is used.
258 */
259 int forumpost_is_owner(int rid, const char *zUserName){
260 return db_int(0, "SELECT 1 FROM event "
261 "WHERE type='f' AND objid=%d "
262 "AND coalesce(euser,user)=%Q",
263 rid, zUserName ? zUserName : login_name());
 
 
 
 
 
 
 
 
 
 
264 }
265
266 /*
267 ** Returns true if p, or any parent of p, has a non-zero iClosed
268 ** value. Returns 0 if !p. For an edited chain of post, the tag is
@@ -397,25 +407,27 @@
397 ** provide consistent behavior, it always acts on the first version of
398 ** the given forum post, walking the forumpost.fprev values to find
399 ** the head of the chain.
400 **
401 ** If addTag is true then a propagating tag is added, except as noted
402 ** below, with the given optional zReason string as the tag's
403 ** value. If addTag is false then any matching active tag on frid is
404 ** cancelled, except as noted below. zReason is ignored if it is NULL
405 ** or starts with a NUL byte, or if addTag is false.
406 **
407 ** This function only adds a tag if forum_rid_is_tagged() indicates
408 ** that frid's head is not tagged. If a parent post is already tagged,
409 ** no tag is added. Similarly, it will only remove a tag from a post
410 ** which has its own tag, and will not remove an inherited one from a
411 ** parent post.
412 **
413 ** If addTag is true and frid is already tagged (directly or
414 ** inherited), this is a no-op. Likewise, if addTag is false and frid
415 ** itself is not tagged (not accounting for an inherited closed tag),
416 ** this is a no-op.
 
 
417 **
418 ** Returns true if it actually creates a new tag, else false. Fails
419 ** fatally on error.
420 **
421 ** If it returns true then state from previously-loaded posts may be
@@ -438,57 +450,62 @@
438 ** - The applied tag is propagating so so that "closed" tags can
439 ** account for how edits of posts are handled. This differs from
440 ** closure of a branch, where a non-propagating tag is used.
441 */
442 static int forumpost_tag(int frid, const char *zTagName, int addTag,
443 const char *zReason){
444 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
445 Blob cksum = BLOB_INITIALIZER; /* Z-card */
446 int iTagged; /* true if frid is already tagged */
447 int trid; /* RID of new control artifact */
448 char *zUuid; /* UUID of head version of post */
449
450 db_begin_transaction();
451 frid = forumpost_head_rid(frid);
452 iTagged = forum_rid_is_tagged(frid, zTagName, 1);
453 if( (iTagged && addTag
454 /* Already tagged, noting that in the case of (addTag<0) it may
455 ** actually be a parent which is tagged. */)
456 || (iTagged<=0 && !addTag
457 /* This entry is not tagged, but a parent post may be. */) ){
458 db_end_transaction(0);
459 return 0;
460 }
461 if( addTag==0 || (zReason && !zReason[0]) ){
462 zReason = 0;
 
 
 
 
 
 
 
 
 
 
 
 
463 }
464 zUuid = rid_to_uuid(frid);
465 blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
466 blob_appendf(&artifact, "T %c%s %s%s%F\n",
467 addTag ? '*' : '-', zTagName,
468 zUuid, zReason ? " " : "", zReason ? zReason : "");
469 blob_appendf(&artifact, "U %F\n", login_name());
470 md5sum_blob(&artifact, &cksum);
471 blob_appendf(&artifact, "Z %b\n", &cksum);
472 blob_reset(&cksum);
473 trid = content_put_ex(&artifact, 0, 0, 0, 0);
474 if( trid==0 ){
475 fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
476 }
477 if( manifest_crosslink(trid, &artifact,
478 MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
479 fossil_fatal("%s", g.zErrMsg);
480 }
481 assert( blob_is_reset(&artifact) );
482 db_add_unsent(trid);
483 admin_log("Tag forum post %S with %c%s",
484 zUuid, addTag ? '*' : '-', zTagName);
485 fossil_free(zUuid);
486 /* Potential TODO: if (iClosed>0) then we could find the initial tag
487 ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
488 ** size of these artifacts, however, that would save little space,
489 ** if any. */
490 db_end_transaction(0);
491 return 1;
492 }
493
494 /*
@@ -853,10 +870,72 @@
853 }
854 @ </table>
855 db_finalize(&q);
856 style_finish_page();
857 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
859 /*
860 ** Render a forum post for display
861 */
862 void forum_render(
@@ -1129,11 +1208,11 @@
1129 /* Provide a link to the raw source code. */
1130 if( !bUnf ){
1131 @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a>
1132 }
1133 @ </h3>
1134 }
1135
1136 /* Check if this post is approved, also if it's by the current user. */
1137 bPrivate = content_is_private(p->fpid);
1138 bSameUser = login_is_individual()
1139 && fossil_strcmp(pManifest->zUser, g.zLogin)==0;
@@ -1193,25 +1272,25 @@
1193 login_insert_csrf_secret();
1194 @ </form>
1195
1196 if( bSelect ){
1197 const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
 
1198 if( forumpost_may_close() && iClosed>=0 ){
1199 @ <form method="post" \
1200 @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
1201 login_insert_csrf_secret();
1202 @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1203 if( moderation_pending(p->fpid)==0 ){
1204 @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1205 @ class='hidden %s(iClosed ? "action-reopen" : "action-close")'/>
 
1206 /* ^^^ activated by fossil.page.forumpost.js */
1207 }
1208 @ </form>
1209 }
1210 if( g.perm.Admin ||
1211 (login_is_individual()
1212 && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){
1213 /* When an admin edits someone else's post, the admin
1214 ** effectively takes over ownership of it (and we currently
1215 ** have no way of passing it back). Because of this, we
1216 ** check the ownership of `p` instead of `pHead`. */
1217 @ <form method="post" action="%R/attachadd">\
@@ -1219,26 +1298,33 @@
1219 @ <input type="submit" value="Attach...">
1220 login_insert_csrf_secret();
1221 moderation_pending_www(p->fpid);
1222 @ </form>
1223 }
1224 if( !p->pIrt && g.perm.Setup ){
1225 const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0);
1226 @ <form method="post" \
1227 @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'>
1228 login_insert_csrf_secret();
1229 @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1230 @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \
1231 @ class='hidden %s(isPinned ? "action-unpin" : "action-pin")'/>
1232 /* ^^^ activated by fossil.page.forumpost.js */
1233 @ </form>
 
 
 
 
1234 }
1235 }
1236 @ </div>
1237 }
 
 
 
1238 @ </div>
1239 }
1240
1241 /* Clean up. */
1242 manifest_destroy(pManifest);
1243 }
1244
@@ -1730,28 +1816,41 @@
1730 mimetype_option_menu(zMimetype, "mimetype");
1731 @ <div class="forum-editor-widget">
1732 @ <textarea aria-label="Content:" name="content" class="wikiedit" \
1733 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div>
1734 }
 
 
 
 
 
 
 
 
 
 
 
 
 
1735
1736 /*
1737 ** Internal helper for /forumpost_XYZ internal pages which tag/untag
1738 ** posts.
1739 */
1740 static void forumpost_action_helper(const char *zTag, const char *zVal,
1741 int addTag){
1742 const char *zFpid = PD("fpid","");
1743 int fpid;
1744
1745 cgi_csrf_verify();
1746 fpid = symbolic_name_to_rid(zFpid, "f");
1747 if( fpid<=0 ){
1748 webpage_error("Missing or invalid fpid query parameter");
1749 }
1750 forumpost_tag(fpid, zTag, addTag, zVal);
1751 cgi_redirectf("%R/forumpost/%S",zFpid);
1752 return;
1753 }
1754
1755 /*
1756 ** WEBPAGE: forumpost_close hidden
1757 ** WEBPAGE: forumpost_reopen hidden
@@ -1769,11 +1868,11 @@
1769 if( forumpost_may_close()==0 ){
1770 login_needed(g.anon.Admin);
1771 }else{
1772 const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0;
1773 char const *zReason = bIsAdd ? 0 : PD("reason", 0);
1774 forumpost_action_helper("closed", zReason, bIsAdd);
1775 }
1776 }
1777
1778 /*
1779 ** WEBPAGE: forumpost_pin hidden
@@ -1790,11 +1889,35 @@
1790 login_check_credentials();
1791 if( !g.perm.Setup ){
1792 login_needed(g.anon.Setup);
1793 }else{
1794 const int bIsAdd = sqlite3_strglob("*_pin*", g.zPath)==0;
1795 forumpost_action_helper("pinned", 0, bIsAdd);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1796 }
1797 }
1798
1799 /*
1800 ** WEBPAGE: forumnew
@@ -2466,11 +2589,11 @@
2466 char *zDuration = human_readable_age(db_column_double(&q,1));
2467 @ %d(nMsg) posts spanning %h(zDuration)\
2468 fossil_free(zDuration);
2469 }
2470 @ </td>\
2471 if( bHasStatus ){
2472 @ <td>%h(zStatus)</td>\
2473 }
2474 @</tr>
2475 fossil_free(zAge);
2476 }
2477
--- src/forum.c
+++ src/forum.c
@@ -255,14 +255,24 @@
255 ** matches the event.(euser,user) field for a formpost entry with the
256 ** matching RID. Returns false if no match is found. If zUserName is
257 ** 0 then login_name() is used.
258 */
259 int forumpost_is_owner(int rid, const char *zUserName){
260 static Stmt q;
261 int rc;
262 if( !q.pStmt ){
263 db_static_prepare(
264 &q, "SELECT 1 FROM event"
265 " WHERE type='f' AND objid=$rid"
266 " AND coalesce(euser,user)=$user"
267 );
268 }
269 db_bind_int(&q, "$rid", rid);
270 db_bind_text(&q, "$user", zUserName ? zUserName : login_name());
271 rc = SQLITE_ROW==db_step(&q);
272 db_reset(&q);
273 return rc;
274 }
275
276 /*
277 ** Returns true if p, or any parent of p, has a non-zero iClosed
278 ** value. Returns 0 if !p. For an edited chain of post, the tag is
@@ -397,25 +407,27 @@
407 ** provide consistent behavior, it always acts on the first version of
408 ** the given forum post, walking the forumpost.fprev values to find
409 ** the head of the chain.
410 **
411 ** If addTag is true then a propagating tag is added, except as noted
412 ** below, with the given optional zValue string as the tag's
413 ** value. If addTag is false then any matching active tag on frid is
414 ** cancelled, except as noted below. zValue is ignored if it is NULL
415 ** or starts with a NUL byte, or if addTag is false.
416 **
417 ** This function only adds a tag if forum_rid_is_tagged() indicates
418 ** that frid's head is not tagged. If a parent post is already tagged,
419 ** no tag is added. Similarly, it will only remove a tag from a post
420 ** which has its own tag, and will not remove an inherited one from a
421 ** parent post.
422 **
423 ** If addTag is true and frid is already tagged, this is a
424 ** no-op. Likewise, if addTag is false and frid is not tagged
425 ** (not accounting for an inherited closed tag), this is a no-op.
426 **
427 ** If bCheckIrt is true then the forum post IRT hierarchy is searched
428 ** for the tag, otherwise only the given RID is checked.
429 **
430 ** Returns true if it actually creates a new tag, else false. Fails
431 ** fatally on error.
432 **
433 ** If it returns true then state from previously-loaded posts may be
@@ -438,57 +450,62 @@
450 ** - The applied tag is propagating so so that "closed" tags can
451 ** account for how edits of posts are handled. This differs from
452 ** closure of a branch, where a non-propagating tag is used.
453 */
454 static int forumpost_tag(int frid, const char *zTagName, int addTag,
455 const char *zValue){
456 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
457 Blob cksum = BLOB_INITIALIZER; /* Z-card */
458 int iTagged; /* true if frid is already tagged */
459 int trid; /* RID of new control artifact */
460 char *zUuid; /* UUID of head version of post */
461
462 db_begin_transaction();
463 frid = forumpost_head_rid(frid);
464 iTagged = forum_rid_is_tagged(frid, zTagName, 0);
465 if( !addTag && !iTagged ){
466 /* Nothing to do. We never tag an IRT-inherited post via this
467 ** function. */
 
 
468 db_end_transaction(0);
469 return 0;
470 }
471 if( !addTag || (zValue && !zValue[0]) ){
472 zValue = 0;
473 }
474 if( addTag && iTagged ){
475 char *zOld = 0;
476 int cmp;
477 rid_has_tag2(iTagged, zTagName, &zOld);
478 cmp = fossil_strcmp(zOld, zValue);
479 fossil_free(zOld);
480 if( 0==cmp ){
481 /* Same value - leave it as is. */
482 db_end_transaction(0);
483 return 0;
484 }
485 }
486 zUuid = rid_to_uuid(frid);
487 blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
488 blob_appendf(&artifact, "T %c%s %s%s%F\n",
489 addTag ? '*' : '-', zTagName,
490 zUuid, zValue ? " " : "", zValue ? zValue : "");
491 blob_appendf(&artifact, "U %F\n", login_name());
492 md5sum_blob(&artifact, &cksum);
493 blob_appendf(&artifact, "Z %b\n", &cksum);
494 blob_reset(&cksum);
495 trid = content_put_ex(&artifact, 0, 0, 0, 0);
496 if( trid==0 ){
497 fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
498 }
499 if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){
 
500 fossil_fatal("%s", g.zErrMsg);
501 }
502 assert( blob_is_reset(&artifact) );
503 db_add_unsent(trid);
504 admin_log("Tag forum post %S with %c%s",
505 zUuid, addTag ? '*' : '-', zTagName);
506 fossil_free(zUuid);
 
 
 
 
507 db_end_transaction(0);
508 return 1;
509 }
510
511 /*
@@ -853,10 +870,72 @@
870 }
871 @ </table>
872 db_finalize(&q);
873 style_finish_page();
874 }
875
876 /*
877 ** Returns true if the current user is authorized to set forum post
878 ** fpid's status.
879 */
880 static int forum_may_set_status(int fpid){
881 return g.perm.Admin
882 || g.perm.ModForum
883 || (login_is_individual()
884 && forumpost_is_owner(fpid, 0));
885 }
886
887 /*
888 ** If the current user is authorized to set fp's status then this
889 ** renders a mini-form for setting the status then redirecting back to
890 ** the post. Else it may emit a status label or no output.
891 */
892 static void forum_render_status_selection( const ForumPost *fp ){
893 const ForumStatusList * const fss = forum_statuses();
894 if( fss->n>1 ){
895 const ForumPost * pHead = fp->pEditHead ? fp->pEditHead : fp;
896 int i;
897 char * zCurrent = 0;
898 const ForumStatus * sCurrent = 0;
899 rid_has_tag2(pHead->fpid, "status", &zCurrent);
900 for( i = 0; i < fss->n; ++i ){
901 const ForumStatus * const fs = &fss->aStatus[i];
902 if( 0==fossil_strcmp(zCurrent, fs->zValue) ){
903 sCurrent = fs;
904 break;
905 }
906 }
907 if( !sCurrent ) sCurrent = &fss->aStatus[0];
908 assert( sCurrent );
909 @ <span class='forum-status-selection'>
910 if( forum_may_set_status(fp->fpid)
911 /* FIXME: only do this if fp is the currently-selected post */ ){
912 @ <form method="post" action='%R/forumpost_status'>
913 login_insert_csrf_secret();
914 @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
915 @ <select name='status' data-fpid='%s(fp->zUuid)>'\
916 @ data-initial-value='%h(zCurrent ? zCurrent : "")'>
917 for( i = 0; i < fss->n; ++i ){
918 const ForumStatus * const fs = &fss->aStatus[i];
919 @ <option value='%h(fs->zValue)'\
920 @ %s(sCurrent==fs ? " selected" : "")>\
921 @ %h(fs->zLabel)</option>
922 }
923 @ </select>
924 @ <input type='button' class='submit action-status' disabled
925 @ value='Change' />
926 /* ^^^ This must be <input>, not <button>, or else tapping it
927 ** will unconditionally submit. */
928 @ </form>
929 /* Form is activated in fossil.page.forumpost.js */
930 }else{
931 @ Status: %h(sCurrent->zLabel);
932 }
933 @ </span>
934 fossil_free(zCurrent);
935 }
936 }
937
938 /*
939 ** Render a forum post for display
940 */
941 void forum_render(
@@ -1129,11 +1208,11 @@
1208 /* Provide a link to the raw source code. */
1209 if( !bUnf ){
1210 @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a>
1211 }
1212 @ </h3>
1213 }/*!bRaw*/
1214
1215 /* Check if this post is approved, also if it's by the current user. */
1216 bPrivate = content_is_private(p->fpid);
1217 bSameUser = login_is_individual()
1218 && fossil_strcmp(pManifest->zUser, g.zLogin)==0;
@@ -1193,25 +1272,25 @@
1272 login_insert_csrf_secret();
1273 @ </form>
1274
1275 if( bSelect ){
1276 const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
1277 const int bIsOwner = forumpost_is_owner(p/*not pHead*/->fpid, 0);
1278 if( forumpost_may_close() && iClosed>=0 ){
1279 @ <form method="post" \
1280 @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
1281 login_insert_csrf_secret();
1282 @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1283 if( moderation_pending(p->fpid)==0 ){
1284 @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1285 @ class='submit hidden \
1286 @ %s(iClosed ? "action-reopen" : "action-close")'/>
1287 /* ^^^ activated by fossil.page.forumpost.js */
1288 }
1289 @ </form>
1290 }
1291 if( g.perm.Admin || (login_is_individual() && bIsOwner) ){
 
 
1292 /* When an admin edits someone else's post, the admin
1293 ** effectively takes over ownership of it (and we currently
1294 ** have no way of passing it back). Because of this, we
1295 ** check the ownership of `p` instead of `pHead`. */
1296 @ <form method="post" action="%R/attachadd">\
@@ -1219,26 +1298,33 @@
1298 @ <input type="submit" value="Attach...">
1299 login_insert_csrf_secret();
1300 moderation_pending_www(p->fpid);
1301 @ </form>
1302 }
1303 if( !p->pIrt ){
1304 /* Root node only... */
1305 if( g.perm.Setup ){
1306 const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0);
1307 @ <form method="post" \
1308 @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'>
1309 login_insert_csrf_secret();
1310 @ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1311 @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \
1312 @ class='submit hidden \
1313 @ %s(isPinned ? "action-unpin" : "action-pin")'/>
1314 /* ^^^ activated by fossil.page.forumpost.js */
1315 @ </form>
1316 }
1317 }
1318 }
1319 @ </div>
1320 }
1321 if( !p->pIrt && (flags & FDISPLAY_SELECTED)){
1322 forum_render_status_selection(p);
1323 }
1324 @ </div>
1325 }/*!bRaw*/
1326
1327 /* Clean up. */
1328 manifest_destroy(pManifest);
1329 }
1330
@@ -1730,28 +1816,41 @@
1816 mimetype_option_menu(zMimetype, "mimetype");
1817 @ <div class="forum-editor-widget">
1818 @ <textarea aria-label="Content:" name="content" class="wikiedit" \
1819 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div>
1820 }
1821
1822 /*
1823 ** If PD("fpid") refers to a forum post, its rid is returned, else
1824 ** this function emits an error does not does return.
1825 */
1826 static int forum_validate_fpid_param(void){
1827 const char *zFpid = PD("fpid","");
1828 int fpid = symbolic_name_to_rid(zFpid, "f");
1829 if( fpid<=0 ){
1830 webpage_error("Missing or invalid fpid parameter.");
1831 }
1832 return fpid;
1833 }
1834
1835 /*
1836 ** Internal helper for /forumpost_XYZ internal pages which tag/untag
1837 ** posts.
1838 */
1839 static void forumpost_action_helper(const char *zTag, const char *zVal,
1840 int addTag, int validFpid){
1841 if( !cgi_csrf_safe(2) ){
1842 webpage_error("CSRF validation failed");
1843 }else{
1844 const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1845 forumpost_tag(fpid, zTag, addTag, zVal);
1846 #if 0
1847 @ DEBUG frid=%d(fpid) addTag=%d(addTag) %h(zTag)=%h(zVal ? zVal : "NULL")
1848 #else
1849 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1850 #endif
1851 }
1852 }
1853
1854 /*
1855 ** WEBPAGE: forumpost_close hidden
1856 ** WEBPAGE: forumpost_reopen hidden
@@ -1769,11 +1868,11 @@
1868 if( forumpost_may_close()==0 ){
1869 login_needed(g.anon.Admin);
1870 }else{
1871 const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0;
1872 char const *zReason = bIsAdd ? 0 : PD("reason", 0);
1873 forumpost_action_helper("closed", zReason, bIsAdd, 0);
1874 }
1875 }
1876
1877 /*
1878 ** WEBPAGE: forumpost_pin hidden
@@ -1790,11 +1889,35 @@
1889 login_check_credentials();
1890 if( !g.perm.Setup ){
1891 login_needed(g.anon.Setup);
1892 }else{
1893 const int bIsAdd = sqlite3_strglob("*_pin*", g.zPath)==0;
1894 forumpost_action_helper("pinned", 0, bIsAdd, 0);
1895 }
1896 }
1897
1898 /*
1899 ** WEBPAGE: forumpost_status hidden
1900 **
1901 ** fpid=X Hash of the post to be edited. REQUIRED
1902 ** status=Y New status value. REQUIRED
1903 **
1904 ** Updates the current status=Y tag on the first version of
1905 ** the forum post X. Requires forum_may_set_status() permissions.
1906 */
1907 void forum_page_status(void){
1908 int fpid;
1909 login_check_credentials();
1910 fpid = forum_validate_fpid_param();
1911 if(forum_may_set_status(fpid)){
1912 const char *zStatus = PD("status",0);
1913 if( !zStatus || !zStatus[0] ){
1914 webpage_error("Missing required status.");
1915 }
1916 forumpost_action_helper("status", zStatus, 1, fpid);
1917 }else{
1918 webpage_error("You lack permissions to change this post's status.");
1919 }
1920 }
1921
1922 /*
1923 ** WEBPAGE: forumnew
@@ -2466,11 +2589,11 @@
2589 char *zDuration = human_readable_age(db_column_double(&q,1));
2590 @ %d(nMsg) posts spanning %h(zDuration)\
2591 fossil_free(zDuration);
2592 }
2593 @ </td>\
2594 if( zStatus ){
2595 @ <td>%h(zStatus)</td>\
2596 }
2597 @</tr>
2598 fossil_free(zAge);
2599 }
2600
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -140,9 +140,36 @@
140140
? "Confirm unpin"
141141
: "Confirm pin"),
142142
onconfirm: ()=>form.submit()
143143
});
144144
});
145
+ form
146
+ .querySelectorAll("input[type='button'].action-status")
147
+ .forEach(function(btn){
148
+ btn.classList.remove('hidden');
149
+ const sel = btn.previousElementSibling;
150
+ const updateAble = ()=>{
151
+ if( sel.dataset.initialValue ){
152
+ if( sel.dataset.initialValue===sel.value ){
153
+ btn.setAttribute('disabled','');
154
+ }else{
155
+ btn.removeAttribute('disabled');
156
+ }
157
+ }else{
158
+ if(sel.selectedIndex===0){
159
+ btn.setAttribute('disabled','');
160
+ }else{
161
+ btn.removeAttribute('disabled');
162
+ }
163
+ }
164
+ };
165
+ sel.addEventListener('change', updateAble, true);
166
+ updateAble();
167
+ F.confirmer(btn, {
168
+ confirmText: "Confirm status change",
169
+ onconfirm: ()=>form.submit()
170
+ });
171
+ });
145172
});
146173
147174
})/*F.onPageLoad callback*/;
148175
})(window.fossil);
149176
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -140,9 +140,36 @@
140 ? "Confirm unpin"
141 : "Confirm pin"),
142 onconfirm: ()=>form.submit()
143 });
144 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145 });
146
147 })/*F.onPageLoad callback*/;
148 })(window.fossil);
149
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -140,9 +140,36 @@
140 ? "Confirm unpin"
141 : "Confirm pin"),
142 onconfirm: ()=>form.submit()
143 });
144 });
145 form
146 .querySelectorAll("input[type='button'].action-status")
147 .forEach(function(btn){
148 btn.classList.remove('hidden');
149 const sel = btn.previousElementSibling;
150 const updateAble = ()=>{
151 if( sel.dataset.initialValue ){
152 if( sel.dataset.initialValue===sel.value ){
153 btn.setAttribute('disabled','');
154 }else{
155 btn.removeAttribute('disabled');
156 }
157 }else{
158 if(sel.selectedIndex===0){
159 btn.setAttribute('disabled','');
160 }else{
161 btn.removeAttribute('disabled');
162 }
163 }
164 };
165 sel.addEventListener('change', updateAble, true);
166 updateAble();
167 F.confirmer(btn, {
168 confirmText: "Confirm status change",
169 onconfirm: ()=>form.submit()
170 });
171 });
172 });
173
174 })/*F.onPageLoad callback*/;
175 })(window.fossil);
176
+35
--- src/tag.c
+++ src/tag.c
@@ -958,10 +958,45 @@
958958
" AND tagxref.tagid=tag.tagid",
959959
rid, tagId
960960
);
961961
}
962962
963
+
964
+/*
965
+** If the given blob.rid value has the given tag applied to it,
966
+** returns false and sets *pOut to a copy of its value (or NULL if it
967
+** has no value). Else returns false and sets *pOut to 0.
968
+** A truthy value returned is the associated tag.tagid value.
969
+**
970
+** Ownership of *pOut is transfered to the caller, who must eventually
971
+** fossil_free() it.
972
+*/
973
+int rid_has_tag2(int rid, const char *zTag, char **pOut){
974
+ static Stmt q;
975
+ int rc = 0;
976
+ if( !q.pStmt ){
977
+ db_prepare(
978
+ &q, "SELECT t.tagid, x.value"
979
+ " FROM tagxref x, tag t"
980
+ " WHERE x.rid=:rid"
981
+ " AND x.tagtype>0"
982
+ " AND x.tagid=t.tagid"
983
+ " AND t.tagname=:name"
984
+ " ORDER BY mtime DESC"
985
+ );
986
+ }
987
+ *pOut = 0;
988
+ db_bind_int(&q, ":rid", rid);
989
+ db_bind_text(&q, ":name", zTag);
990
+ if( SQLITE_ROW==db_step(&q) ){
991
+ rc = db_column_int(&q, 0);
992
+ *pOut = fossil_strdup(db_column_text(&q, 1));
993
+ }
994
+ db_reset(&q);
995
+ return rc;
996
+}
997
+
963998
964999
/*
9651000
** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry
9661001
** of an active (non-cancelled) tag matching the given rid and tag
9671002
** name string, else returns 0. This function does not distinguish
9681003
--- src/tag.c
+++ src/tag.c
@@ -958,10 +958,45 @@
958 " AND tagxref.tagid=tag.tagid",
959 rid, tagId
960 );
961 }
962
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
963
964 /*
965 ** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry
966 ** of an active (non-cancelled) tag matching the given rid and tag
967 ** name string, else returns 0. This function does not distinguish
968
--- src/tag.c
+++ src/tag.c
@@ -958,10 +958,45 @@
958 " AND tagxref.tagid=tag.tagid",
959 rid, tagId
960 );
961 }
962
963
964 /*
965 ** If the given blob.rid value has the given tag applied to it,
966 ** returns false and sets *pOut to a copy of its value (or NULL if it
967 ** has no value). Else returns false and sets *pOut to 0.
968 ** A truthy value returned is the associated tag.tagid value.
969 **
970 ** Ownership of *pOut is transfered to the caller, who must eventually
971 ** fossil_free() it.
972 */
973 int rid_has_tag2(int rid, const char *zTag, char **pOut){
974 static Stmt q;
975 int rc = 0;
976 if( !q.pStmt ){
977 db_prepare(
978 &q, "SELECT t.tagid, x.value"
979 " FROM tagxref x, tag t"
980 " WHERE x.rid=:rid"
981 " AND x.tagtype>0"
982 " AND x.tagid=t.tagid"
983 " AND t.tagname=:name"
984 " ORDER BY mtime DESC"
985 );
986 }
987 *pOut = 0;
988 db_bind_int(&q, ":rid", rid);
989 db_bind_text(&q, ":name", zTag);
990 if( SQLITE_ROW==db_step(&q) ){
991 rc = db_column_int(&q, 0);
992 *pOut = fossil_strdup(db_column_text(&q, 1));
993 }
994 db_reset(&q);
995 return rc;
996 }
997
998
999 /*
1000 ** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry
1001 ** of an active (non-cancelled) tag matching the given rid and tag
1002 ** name string, else returns 0. This function does not distinguish
1003

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button