1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
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
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
|
/*
** Copyright (c) 2012 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
** drh@hwaci.com
** http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code used to deal with moderator actions for
** Wiki and Tickets.
*/
#include "config.h"
#include "moderate.h"
#include <assert.h>
/*
** Create a table to represent pending moderation requests, if the
** table does not already exist.
*/
void moderation_table_create(void){
db_multi_exec(
"CREATE TABLE IF NOT EXISTS repository.modreq(\n"
" objid INTEGER PRIMARY KEY,\n" /* Record pending approval */
" attachRid INT,\n" /* Object attached */
" tktid TEXT\n" /* Associated ticket id */
");\n"
);
}
/*
** Return TRUE if the modreq table exists
*/
int moderation_table_exists(void){
return db_table_exists("repository", "modreq");
}
/*
** Return TRUE if the object specified is being held for moderation.
*/
int moderation_pending(int rid){
static Stmt q;
int rc;
if( rid==0 || !moderation_table_exists() ) return 0;
db_static_prepare(&q, "SELECT 1 FROM modreq WHERE objid=:objid");
db_bind_int(&q, ":objid", rid);
rc = db_step(&q)==SQLITE_ROW;
db_reset(&q);
return rc;
}
/*
** If the rid object is being held for moderation, write out
** an "awaiting moderation" message and return true.
**
** If the object is not being held for moderation, simply return
** false without generating any output.
*/
int moderation_pending_www(int rid){
int pending = moderation_pending(rid);
if( pending ){
#if 0
if( moderation_user_could(rid, 1, 0) ){
/* It would be nice to emit a link to the appropriate page to
** approve/reject the moderation, but for that we need
** artifact-type-dependent info and links. That's complicated by
** the fact that deriving whether rid refers to an attachment or
** an attachment target is apparently tricky because of how
** attachments are recorded in the event table. */
@ <span class="modpending">(<a href="%R/WHAT_GOES_HERE?">\
@Awaiting Moderator Approval</a>)</span>
}else
#endif
{
@ <span class="modpending">(Awaiting Moderator Approval)</span>
}
}
return pending;
}
/*
** Return TRUE if there any pending moderation requests.
*/
int moderation_needed(void){
if( !moderation_table_exists() ) return 0;
return db_exists("SELECT 1 FROM modreq");
}
/*
** Check to see if the object identified by RID is used for anything.
*/
static int object_used(int rid){
static const char *const aTabField[] = {
"modreq", "attachRid",
"mlink", "mid",
"mlink", "fid",
"tagxref", "srcid",
"tagxref", "rid",
};
int i;
for(i=0; i<count(aTabField); i+=2){
if( db_exists("SELECT 1 FROM \"%w\" WHERE \"%w\"=%d",
aTabField[i], aTabField[i+1], rid) ) return 1;
}
return 0;
}
/*
** Delete a moderation item given by objid
*/
void moderation_disapprove(int objid){
Stmt q;
char *zTktid;
int attachRid = 0;
int rid;
if( !moderation_pending(objid) ) return;
db_begin_transaction();
rid = objid;
while( rid && content_is_private(rid) ){
db_prepare(&q, "SELECT rid FROM delta WHERE srcid=%d", rid);
while( db_step(&q)==SQLITE_ROW ){
int ridUser = db_column_int(&q, 0);
content_undelta(ridUser);
}
db_finalize(&q);
db_multi_exec(
"DELETE FROM blob WHERE rid=%d;"
"DELETE FROM delta WHERE rid=%d;"
"DELETE FROM event WHERE objid=%d;"
"DELETE FROM tagxref WHERE rid=%d;"
"DELETE FROM private WHERE rid=%d;"
"DELETE FROM attachment WHERE attachid=%d;",
rid, rid, rid, rid, rid, rid
);
if( db_table_exists("repository","forumpost") ){
db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid);
}
zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid);
if( zTktid && zTktid[0] ){
ticket_rebuild_entry(zTktid);
fossil_free(zTktid);
}
attachRid = db_int(0, "SELECT attachRid FROM modreq WHERE objid=%d", rid);
if( rid==objid ){
db_multi_exec("DELETE FROM modreq WHERE objid=%d", rid);
}
if( attachRid && object_used(attachRid) ) attachRid = 0;
admin_log("Disapproved moderation of rid %d.", rid);
rid = attachRid;
}
db_end_transaction(0);
}
/*
** Approve an object held for moderation.
*/
void moderation_approve(char class, int rid){
if( !moderation_pending(rid) ) return;
db_begin_transaction();
db_multi_exec(
"DELETE FROM private WHERE rid=%d;"
"INSERT OR IGNORE INTO unclustered VALUES(%d);"
"INSERT OR IGNORE INTO unsent VALUES(%d);",
rid, rid, rid
);
db_multi_exec("DELETE FROM modreq WHERE objid=%d", rid);
admin_log("Approved moderation of rid %c-%d.", class, rid);
if( class!='a' ) search_doc_touch(class, rid, 0);
setup_incr_cfgcnt();
db_end_transaction(0);
}
/*
** WEBPAGE: modreq
**
** Show all pending moderation request
*/
void modreq_page(void){
Blob sql;
Stmt q;
login_check_credentials();
if( !g.perm.ModWiki && !g.perm.ModTkt && !g.perm.ModForum ){
login_needed(g.anon.ModWiki && g.anon.ModTkt && g.anon.ModForum);
return;
}
style_header("Pending Moderation Requests");
@ <h2>All Pending Moderation Requests</h2>
if( moderation_table_exists() ){
blob_init(&sql, timeline_query_for_www(), -1);
blob_append_sql(&sql,
" AND event.objid IN (SELECT objid FROM modreq)"
" ORDER BY event.mtime DESC"
);
db_prepare(&q, "%s", blob_sql_text(&sql));
www_print_timeline(&q, 0, 0);
db_finalize(&q);
}
style_finish_page();
}
/*
** Disapproves any entries in the modreq table which belong to any
** user whose name is no longer found in the user table. This is only
** intended to be called after user deletion via /setup_uedit.
**
** To figure out whether a name exists it cross-references
** coalesce(event.euser, event.user) with user.login, limiting the
** selection to event entries where objid matches an entry in the
** modreq table.
**
** This is a no-op if called without g.perm.Admin permissions or if
** moderation_table_exists() returns false.
*/
void moderation_disapprove_for_missing_users(){
Stmt q;
if( !g.perm.Admin || !moderation_table_exists() ){
return;
}
db_begin_transaction();
db_prepare(&q,
"SELECT objid FROM event WHERE objid IN "
"(SELECT objid FROM modreq) "
"AND coalesce(euser,user) NOT IN "
"(SELECT login FROM user)"
);
while( db_step(&q)==SQLITE_ROW ){
int const objid = db_column_int(&q, 0);
moderation_disapprove(objid);
}
db_finalize(&q);
setup_incr_cfgcnt();
db_end_transaction(0);
}
/*
** Returns true if the current user could ostensibly moderate the blob
** refered to by rid, irrespective of whether that object is currently
** pending moderation. If rid is not an event.objid value then this
** returns 0.
**
** If bMayDeny is true then a matching user is permitted to moderate a
** decline by not an approval. Pass 1 here if true should be returned
** if the current user matches the artifact. When passing false, it
** will only return true for users who have explicit moderation
** permissions. The purpose of this is to exclude pending-moderation
** from the current user in some contexts but not others.
**
** zWho is an optional user name to consider for ownership of an
** artifact, as compared to the artifact's matching event.(euser,user)
** fields. If 0 then it defaults to login_name(). This is strictly a
** name comparison - it does not inspect zWho's repo-level
** permissions.
**
** Design issue: since this gets its info from the event table, it
** cannot unambiguously distinguish between an attachment-capable
** artifact type and attachments to one. Attachment events are encoded
** with type=X, where X is the same as the artifact type to which the
** attachment was applied.
**
** The moderation rules applied here are:
**
** - Admins may always moderate. This is a fast path which bypasses
** artifact lookup. For non-admins, we look for a record in the
** event table.
**
** - Forum, Wiki, and Ticket moderators may always moderate a matching
** artifact. If bMayDeny is true then an artifact's owner, even if
** not a moderator, may moderate it. i.e. a non-moderator owner can
** reject their pending-moderation objects but they may not approve
** them.
**
** - Returns 0 for all other artifact types except that it will always
** return true for admins because that check skips looking at the
** db.
**
*/
int moderation_user_could(int rid, int bMayDeny, const char *zWho){
static Stmt q;
int rc = 0;
if( g.perm.Admin ) return g.perm.Admin;
if( !q.pStmt ){
db_static_prepare(
&q,
"SELECT coalesce(euser,user)=:user, type FROM event "
"WHERE objid=:rid"
);
}
db_bind_int(&q, ":rid", rid);
db_bind_text(&q, ":user", zWho ? zWho : login_name());
if( SQLITE_ROW==db_step(&q) ){
const int bIsOwner = db_column_int(&q, 0);
const char *zType = db_column_text(&q, 1);
switch( zType ? zType[0] : 0 ){
case 'f': rc = g.perm.ModForum || (bIsOwner && bMayDeny); break;
case 't': rc = g.perm.ModTkt || (bIsOwner && bMayDeny); break;
case 'w': rc = g.perm.ModWiki || (bIsOwner && bMayDeny); break;
/* case 'e': Technotes and their attachments are not subject
** to moderation. */
default: break;
}
}
db_reset(&q);
return rc;
}
/*
** COMMAND: test-user-could-moderate
**
** Usage: %fossil test-user-could-moderate ?-deny? user-name ...artifactNames
**
** Tests whether a given user would have the ability to moderate
** the given artifacts. The -deny flag indicates that the check should
** permit moderation if the artifact is owned by the same user.
*/
void test_moderation_user_could_cmd(void){
const char *zWho;
const int bMayDeny = find_option("deny",0,0) != 0;
char * zCap;
int i;
db_find_and_open_repository(0,0);
verify_all_options();
if( g.argc<4 ){
usage("user-name artifact-names...");
}
zWho = g.zLogin = g.argv[2];
zCap = db_text(
0, "SELECT cap FROM user WHERE login=%Q", zWho
);
if( !zCap ){
fossil_fatal("Cannot determine capabilities of user %s", zWho);
}
login_set_capabilities(zCap, 0);
fossil_print("User: %s\nCaps: %s\n", zWho, zCap);
fossil_free(zCap);
for( i = 3; i < g.argc; ++i ){
const char * zArg = g.argv[i];
int rid = symbolic_name_to_rid(zArg, "*");
int may;
if( rid<=0 ){
fossil_fatal("Cannot resolve name: %s", zArg);
}
may = moderation_user_could(rid, bMayDeny, zWho);
fossil_print("%s\t\t=> %d\t=> %s\n", zArg, rid, may ? "yes" : "no");
}
}
|