diff options
Diffstat (limited to 'src/moderate.c')
| -rw-r--r-- | src/moderate.c | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/src/moderate.c b/src/moderate.c new file mode 100644 index 0000000..1d7f379 --- /dev/null +++ b/src/moderate.c @@ -0,0 +1,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"); + } +} |
