Fossil SCM

/attachlist now elides entries which are pending moderation unless they belong to the current user or a moderator.

stephan 2026-05-24 08:57 UTC forum-attachments
Commit 686057e0e6b6d7ea4001f14d85c11a62893ca641680790cbf1efb63a490025e4
2 files changed +26 -10 +103
+26 -10
--- src/attach.c
+++ src/attach.c
@@ -127,29 +127,45 @@
127127
}
128128
blob_append_sql(&sql, " ORDER BY mtime DESC");
129129
db_prepare(&q, "%s", blob_sql_text(&sql));
130130
@ <ol>
131131
while( db_step(&q)==SQLITE_ROW ){
132
- const char *zDate = db_column_text(&q, 0);
133
- const char *zSrc = db_column_text(&q, 1);
134
- const char *zTarget = db_column_text(&q, 2);
135
- const char *zFilename = db_column_text(&q, 3);
136
- const char *zComment = db_column_text(&q, 4);
137
- const char *zUser = db_column_text(&q, 5);
138
- const char *zUuid = db_column_text(&q, 6);
132
+ const char *zDate;
133
+ const char *zSrc;
134
+ const char *zTarget;
135
+ const char *zFilename;
136
+ const char *zComment;
137
+ const char *zUser;
138
+ const char *zUuid;
139
+ const char *zDispUser;
139140
const int attachid = db_column_int(&q, 7);
140
- /* type 0 is a wiki page, 1 is a ticket, 2 is a tech note */
141
- const int type = attachment_target_type(zTarget);
142
- const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
141
+ int type;
143142
int i;
144143
char *zUrlTail = 0;
144
+
145
+ if( moderation_pending(attachid)
146
+ && !moderation_user_could(attachid, 1, 0) ){
147
+ /* Elide entries which are currently pending moderation unless
148
+ ** the user would be able to moderate the entry themselves. */
149
+ continue;
150
+ }
151
+
152
+ zDate = db_column_text(&q, 0);
153
+ zSrc = db_column_text(&q, 1);
154
+ zTarget = db_column_text(&q, 2);
155
+ zFilename = db_column_text(&q, 3);
156
+ zComment = db_column_text(&q, 4);
157
+ zUser = db_column_text(&q, 5);
158
+ zUuid = db_column_text(&q, 6);
159
+ zDispUser = zUser && zUser[0] ? zUser : "anonymous";
145160
for(i=0; zFilename[i]; i++){
146161
if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
147162
zFilename = &zFilename[i+1];
148163
i = -1;
149164
}
150165
}
166
+ type = attachment_target_type(zTarget);
151167
switch( type ){
152168
case CFTYPE_TICKET:
153169
zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
154170
break;
155171
case CFTYPE_EVENT:
156172
--- src/attach.c
+++ src/attach.c
@@ -127,29 +127,45 @@
127 }
128 blob_append_sql(&sql, " ORDER BY mtime DESC");
129 db_prepare(&q, "%s", blob_sql_text(&sql));
130 @ <ol>
131 while( db_step(&q)==SQLITE_ROW ){
132 const char *zDate = db_column_text(&q, 0);
133 const char *zSrc = db_column_text(&q, 1);
134 const char *zTarget = db_column_text(&q, 2);
135 const char *zFilename = db_column_text(&q, 3);
136 const char *zComment = db_column_text(&q, 4);
137 const char *zUser = db_column_text(&q, 5);
138 const char *zUuid = db_column_text(&q, 6);
 
139 const int attachid = db_column_int(&q, 7);
140 /* type 0 is a wiki page, 1 is a ticket, 2 is a tech note */
141 const int type = attachment_target_type(zTarget);
142 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
143 int i;
144 char *zUrlTail = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145 for(i=0; zFilename[i]; i++){
146 if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
147 zFilename = &zFilename[i+1];
148 i = -1;
149 }
150 }
 
151 switch( type ){
152 case CFTYPE_TICKET:
153 zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
154 break;
155 case CFTYPE_EVENT:
156
--- src/attach.c
+++ src/attach.c
@@ -127,29 +127,45 @@
127 }
128 blob_append_sql(&sql, " ORDER BY mtime DESC");
129 db_prepare(&q, "%s", blob_sql_text(&sql));
130 @ <ol>
131 while( db_step(&q)==SQLITE_ROW ){
132 const char *zDate;
133 const char *zSrc;
134 const char *zTarget;
135 const char *zFilename;
136 const char *zComment;
137 const char *zUser;
138 const char *zUuid;
139 const char *zDispUser;
140 const int attachid = db_column_int(&q, 7);
141 int type;
 
 
142 int i;
143 char *zUrlTail = 0;
144
145 if( moderation_pending(attachid)
146 && !moderation_user_could(attachid, 1, 0) ){
147 /* Elide entries which are currently pending moderation unless
148 ** the user would be able to moderate the entry themselves. */
149 continue;
150 }
151
152 zDate = db_column_text(&q, 0);
153 zSrc = db_column_text(&q, 1);
154 zTarget = db_column_text(&q, 2);
155 zFilename = db_column_text(&q, 3);
156 zComment = db_column_text(&q, 4);
157 zUser = db_column_text(&q, 5);
158 zUuid = db_column_text(&q, 6);
159 zDispUser = zUser && zUser[0] ? zUser : "anonymous";
160 for(i=0; zFilename[i]; i++){
161 if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
162 zFilename = &zFilename[i+1];
163 i = -1;
164 }
165 }
166 type = attachment_target_type(zTarget);
167 switch( type ){
168 case CFTYPE_TICKET:
169 zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
170 break;
171 case CFTYPE_EVENT:
172
--- src/moderate.c
+++ src/moderate.c
@@ -225,5 +225,108 @@
225225
}
226226
db_finalize(&q);
227227
setup_incr_cfgcnt();
228228
db_end_transaction(0);
229229
}
230
+
231
+/*
232
+** Returns true if the current user could ostensibly moderate the blob
233
+** refered to by rid, irrespective of whether that object is currently
234
+** pending moderation. If rid is not an event.objid value then this
235
+** returns 0.
236
+**
237
+** If bMayDeny is true then a matching user is permitted to moderate a
238
+** decline by not an approval. Pass 1 here if true should be returned
239
+** if the current user matches the artifact. When passing false, it
240
+** will only return true for users who have explicit moderation
241
+** permissions. The purpose of this is to exclude pending-moderation
242
+** from the current user in some contexts but not others.
243
+**
244
+** zWho is an optional user name to consider for ownership. If 0 it
245
+** defaults to login_name().
246
+**
247
+** The moderation rules applied here are:
248
+**
249
+** - Admins may always moderate. This is a fast path which bypasses
250
+** artifact lookup. For non-admins, we look for a record in the
251
+** event table.
252
+**
253
+** - Forum, Wiki, and Ticket moderators may always moderate a matching
254
+** artifact. If bMayDeny is true then an artifact's owner, even if
255
+** not a moderator, may moderate it. i.e. a non-moderator owner can
256
+** reject their pending-moderation objects but they may not approve
257
+** them.
258
+**
259
+** - Returns 0 for all other artifact types except that it will always
260
+** return true for admins because that's
261
+**
262
+ */
263
+int moderation_user_could(int rid, int bMayDeny, const char *zWho){
264
+ static Stmt q;
265
+ int rc = 0;
266
+ if( g.perm.Admin ) return g.perm.Admin;
267
+ if( !q.pStmt ){
268
+ db_static_prepare(
269
+ &q,
270
+ "SELECT coalesce(euser,user)=:user, type FROM event "
271
+ "WHERE objid=:rid"
272
+ );
273
+ }
274
+ db_bind_int(&q, ":rid", rid);
275
+ db_bind_text(&q, ":user", zWho ? zWho : login_name());
276
+ if( SQLITE_ROW==db_step(&q) ){
277
+ const int bIsOwner = db_column_int(&q, 0);
278
+ const char *zType = db_column_text(&q, 1);
279
+ switch( zType ? zType[0] : 0 ){
280
+ case 'f': rc = g.perm.ModForum || (bIsOwner && bMayDeny); break;
281
+ case 't': rc = g.perm.ModTkt || (bIsOwner && bMayDeny); break;
282
+ case 'w': rc = g.perm.ModWiki || (bIsOwner && bMayDeny); break;
283
+ /* case 'e': Technotes and their attachments are not subject
284
+ ** to moderation. */
285
+ default: break;
286
+ }
287
+ }
288
+ db_reset(&q);
289
+ return rc;
290
+}
291
+
292
+
293
+/*
294
+** COMMAND: test-user-could-moderate
295
+**
296
+** Usage: %d test-user-could-modeate ?-deny? user-name ...artifactNames
297
+**
298
+** Tests whether a given user would have the ability to moderate
299
+** the given artifacts. The -deny flag indicates that the check should
300
+** permit moderation if the artifact is owned by the same user.
301
+*/
302
+void test_moderation_user_could_cmd(void){
303
+ const char *zWho;
304
+ const int bMayDeny = find_option("deny",0,0) != 0;
305
+ char * zCap;
306
+ int i;
307
+ db_find_and_open_repository(0,0);
308
+ verify_all_options();
309
+ if( g.argc<4 ){
310
+ usage("user-name artifact-names...");
311
+ }
312
+ zWho = g.zLogin = g.argv[2];
313
+ zCap = db_text(
314
+ 0, "SELECT cap FROM user WHERE login=%Q", zWho
315
+ );
316
+ if( !zCap ){
317
+ fossil_fatal("Cannot determine capabilities of user %s", zWho);
318
+ }
319
+ login_set_capabilities(zCap, 0);
320
+ fossil_print("User: %s\nCaps: %s\n", zWho, zCap);
321
+ fossil_free(zCap);
322
+ for( i = 3; i < g.argc; ++i ){
323
+ const char * zArg = g.argv[i];
324
+ int rid = symbolic_name_to_rid(zArg, "*");
325
+ int may;
326
+ if( rid<=0 ){
327
+ fossil_fatal("Cannot resolve name: %s", zArg);
328
+ }
329
+ may = moderation_user_could(rid, bMayDeny, zWho);
330
+ fossil_print("%s\t\t=> %d\t=> %s\n", zArg, rid, may ? "yes" : "no");
331
+ }
332
+}
230333
--- src/moderate.c
+++ src/moderate.c
@@ -225,5 +225,108 @@
225 }
226 db_finalize(&q);
227 setup_incr_cfgcnt();
228 db_end_transaction(0);
229 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
--- src/moderate.c
+++ src/moderate.c
@@ -225,5 +225,108 @@
225 }
226 db_finalize(&q);
227 setup_incr_cfgcnt();
228 db_end_transaction(0);
229 }
230
231 /*
232 ** Returns true if the current user could ostensibly moderate the blob
233 ** refered to by rid, irrespective of whether that object is currently
234 ** pending moderation. If rid is not an event.objid value then this
235 ** returns 0.
236 **
237 ** If bMayDeny is true then a matching user is permitted to moderate a
238 ** decline by not an approval. Pass 1 here if true should be returned
239 ** if the current user matches the artifact. When passing false, it
240 ** will only return true for users who have explicit moderation
241 ** permissions. The purpose of this is to exclude pending-moderation
242 ** from the current user in some contexts but not others.
243 **
244 ** zWho is an optional user name to consider for ownership. If 0 it
245 ** defaults to login_name().
246 **
247 ** The moderation rules applied here are:
248 **
249 ** - Admins may always moderate. This is a fast path which bypasses
250 ** artifact lookup. For non-admins, we look for a record in the
251 ** event table.
252 **
253 ** - Forum, Wiki, and Ticket moderators may always moderate a matching
254 ** artifact. If bMayDeny is true then an artifact's owner, even if
255 ** not a moderator, may moderate it. i.e. a non-moderator owner can
256 ** reject their pending-moderation objects but they may not approve
257 ** them.
258 **
259 ** - Returns 0 for all other artifact types except that it will always
260 ** return true for admins because that's
261 **
262 */
263 int moderation_user_could(int rid, int bMayDeny, const char *zWho){
264 static Stmt q;
265 int rc = 0;
266 if( g.perm.Admin ) return g.perm.Admin;
267 if( !q.pStmt ){
268 db_static_prepare(
269 &q,
270 "SELECT coalesce(euser,user)=:user, type FROM event "
271 "WHERE objid=:rid"
272 );
273 }
274 db_bind_int(&q, ":rid", rid);
275 db_bind_text(&q, ":user", zWho ? zWho : login_name());
276 if( SQLITE_ROW==db_step(&q) ){
277 const int bIsOwner = db_column_int(&q, 0);
278 const char *zType = db_column_text(&q, 1);
279 switch( zType ? zType[0] : 0 ){
280 case 'f': rc = g.perm.ModForum || (bIsOwner && bMayDeny); break;
281 case 't': rc = g.perm.ModTkt || (bIsOwner && bMayDeny); break;
282 case 'w': rc = g.perm.ModWiki || (bIsOwner && bMayDeny); break;
283 /* case 'e': Technotes and their attachments are not subject
284 ** to moderation. */
285 default: break;
286 }
287 }
288 db_reset(&q);
289 return rc;
290 }
291
292
293 /*
294 ** COMMAND: test-user-could-moderate
295 **
296 ** Usage: %d test-user-could-modeate ?-deny? user-name ...artifactNames
297 **
298 ** Tests whether a given user would have the ability to moderate
299 ** the given artifacts. The -deny flag indicates that the check should
300 ** permit moderation if the artifact is owned by the same user.
301 */
302 void test_moderation_user_could_cmd(void){
303 const char *zWho;
304 const int bMayDeny = find_option("deny",0,0) != 0;
305 char * zCap;
306 int i;
307 db_find_and_open_repository(0,0);
308 verify_all_options();
309 if( g.argc<4 ){
310 usage("user-name artifact-names...");
311 }
312 zWho = g.zLogin = g.argv[2];
313 zCap = db_text(
314 0, "SELECT cap FROM user WHERE login=%Q", zWho
315 );
316 if( !zCap ){
317 fossil_fatal("Cannot determine capabilities of user %s", zWho);
318 }
319 login_set_capabilities(zCap, 0);
320 fossil_print("User: %s\nCaps: %s\n", zWho, zCap);
321 fossil_free(zCap);
322 for( i = 3; i < g.argc; ++i ){
323 const char * zArg = g.argv[i];
324 int rid = symbolic_name_to_rid(zArg, "*");
325 int may;
326 if( rid<=0 ){
327 fossil_fatal("Cannot resolve name: %s", zArg);
328 }
329 may = moderation_user_could(rid, bMayDeny, zWho);
330 fossil_print("%s\t\t=> %d\t=> %s\n", zArg, rid, may ? "yes" : "no");
331 }
332 }
333

Keyboard Shortcuts

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