X-Git-Url: https://code.citadel.org/?a=blobdiff_plain;f=citadel%2Fmodules%2Fwiki%2Fserv_wiki.c;h=f5c7ebbe2cc175561c7987e6206d29a6409feb9e;hb=bd84d196310b1ca8e3f1298de31f050dbef78fd9;hp=d9db654efbb890b1de7f66299872d9e46e186da5;hpb=6409c4b4d1357185f3f466e4f07c1e3031bf5d2c;p=citadel.git diff --git a/citadel/modules/wiki/serv_wiki.c b/citadel/modules/wiki/serv_wiki.c index d9db654ef..f5c7ebbe2 100644 --- a/citadel/modules/wiki/serv_wiki.c +++ b/citadel/modules/wiki/serv_wiki.c @@ -1,9 +1,7 @@ /* - * $Id$ - * - * Server-side module for Wiki rooms. This will handle things like version control. + * Server-side module for Wiki rooms. This handles things like version control. * - * Copyright (c) 2009-2009 by the citadel.org team + * Copyright (c) 2009 by the citadel.org team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -52,14 +50,27 @@ #include "support.h" #include "config.h" #include "control.h" -#include "room_ops.h" #include "user_ops.h" -#include "policy.h" #include "database.h" #include "msgbase.h" #include "euidindex.h" #include "ctdl_module.h" +/* + * Data passed back and forth between wiki_rev() and its MIME parser callback + */ +struct HistoryEraserCallBackData { + char *tempfilename; /* name of temp file being patched */ + char *stop_when; /* stop when we hit this uuid */ + int done; /* set to nonzero when we're done patching */ +}; + +/* + * Name of the temporary room we create to store old revisions when someone requests them. + * We put it in an invalid namespace so the DAP cleans up after us later. + */ +char *wwm = "9999999999.WikiWaybackMachine"; + /* * Before allowing a wiki page save to execute, we have to perform version control. * This involves fetching the old version of the page if it exists. @@ -77,7 +88,7 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { int rv; char history_page[1024]; char boundary[256]; - char endary[260]; + char prefixed_boundary[258]; char buf[1024]; int nbytes = 0; char *diffbuf = NULL; @@ -94,8 +105,10 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { /* If this isn't a MIME message, don't bother. */ if (msg->cm_format_type != 4) return(0); - /* If there's no EUID we can't do this. */ - if (msg->cm_fields['E'] == NULL) return(0); + /* If there's no EUID we can't do this. Reject the post. */ + if (msg->cm_fields['E'] == NULL) return(1); + + snprintf(history_page, sizeof history_page, "%s_HISTORY_", msg->cm_fields['E']); /* Make sure we're saving a real wiki page rather than a wiki history page. * This is important in order to avoid recursing infinitely into this hook. @@ -110,25 +123,30 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { /* If there's no message text, obviously this is all b0rken and shouldn't happen at all */ if (msg->cm_fields['M'] == NULL) return(0); - /* See if we can retrieve the previous version. */ - old_msgnum = locate_message_by_euid(msg->cm_fields['E'], &CCC->room); - if (old_msgnum <= 0L) return(0); - snprintf(history_page, sizeof history_page, "%s_HISTORY_", msg->cm_fields['E']); - - old_msg = CtdlFetchMessage(old_msgnum, 1); + /* Set the message subject identical to the page name */ + if (msg->cm_fields['U'] != NULL) { + free(msg->cm_fields['U']); + } + msg->cm_fields['U'] = strdup(msg->cm_fields['E']); - if (old_msg != NULL) { + /* See if we can retrieve the previous version. */ + old_msgnum = CtdlLocateMessageByEuid(msg->cm_fields['E'], &CCC->room); + if (old_msgnum > 0L) { + old_msg = CtdlFetchMessage(old_msgnum, 1); + } + else { + old_msg = NULL; + } - if (old_msg->cm_fields['M'] == NULL) { /* old version is corrupt? */ - CtdlFreeMessage(old_msg); - return(0); - } + if ((old_msg != NULL) && (old_msg->cm_fields['M'] == NULL)) { /* old version is corrupt? */ + CtdlFreeMessage(old_msg); + old_msg = NULL; + } - /* If no changes were made, don't bother saving it again */ - if (!strcmp(msg->cm_fields['M'], old_msg->cm_fields['M'])) { - CtdlFreeMessage(old_msg); - return(1); - } + /* If no changes were made, don't bother saving it again */ + if ((old_msg != NULL) && (!strcmp(msg->cm_fields['M'], old_msg->cm_fields['M']))) { + CtdlFreeMessage(old_msg); + return(1); } /* @@ -152,8 +170,8 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { diffbuf = NULL; snprintf(diff_cmd, sizeof diff_cmd, "diff -u %s %s", - ((old_msg != NULL) ? diff_old_filename : "/dev/null"), - diff_new_filename + diff_new_filename, + ((old_msg != NULL) ? diff_old_filename : "/dev/null") ); fp = popen(diff_cmd, "r"); if (fp != NULL) { @@ -163,7 +181,9 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { diffbuf_len += nbytes; } while (nbytes == 1024); diffbuf[diffbuf_len] = 0; - pclose(fp); + if (pclose(fp) != 0) { + CtdlLogPrintf(CTDL_ERR, "pclose() returned an error - diff failed\n"); + } } CtdlLogPrintf(CTDL_DEBUG, "diff length is %d bytes\n", diffbuf_len); @@ -181,7 +201,7 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { /* Now look for the existing edit history */ - history_msgnum = locate_message_by_euid(history_page, &CCC->room); + history_msgnum = CtdlLocateMessageByEuid(history_page, &CCC->room); history_msg = NULL; if (history_msgnum > 0L) { history_msg = CtdlFetchMessage(history_msgnum, 1); @@ -197,11 +217,15 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { history_msg->cm_fields['A'] = strdup("Citadel"); history_msg->cm_fields['R'] = strdup(CCC->room.QRname); history_msg->cm_fields['E'] = strdup(history_page); + history_msg->cm_fields['U'] = strdup(history_page); + history_msg->cm_fields['1'] = strdup("1"); /* suppress full text indexing */ snprintf(boundary, sizeof boundary, "Citadel--Multipart--%04x--%08lx", getpid(), time(NULL)); history_msg->cm_fields['M'] = malloc(1024); snprintf(history_msg->cm_fields['M'], 1024, "Content-type: multipart/mixed; boundary=\"%s\"\n\n" "This is a Citadel wiki history encoded as multipart MIME.\n" + "Each part is comprised of a diff script representing one change set.\n" + "\n" "--%s--\n" , boundary, boundary @@ -210,7 +234,14 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { /* Update the history message (regardless of whether it's new or existing) */ - /* First, figure out the boundary string. We do this even when we generated the + /* Remove the Message-ID from the old version of the history message. This will cause a brand + * new one to be generated, avoiding an uninitentional hit of the loop zapper when we replicate. + */ + if (history_msg->cm_fields['I'] != NULL) { + free(history_msg->cm_fields['I']); + } + + /* Figure out the boundary string. We do this even when we generated the * boundary string in the above code, just to be safe and consistent. */ strcpy(boundary, ""); @@ -240,26 +271,42 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { } } while ( (IsEmptyStr(boundary)) && (*ptr != 0) ); + /* Now look for the first boundary. That is where we need to insert our fun. + */ if (!IsEmptyStr(boundary)) { - snprintf(endary, sizeof endary, "--%s--", boundary); + snprintf(prefixed_boundary, sizeof prefixed_boundary, "--%s", boundary); history_msg->cm_fields['M'] = realloc(history_msg->cm_fields['M'], - strlen(history_msg->cm_fields['M']) + strlen(diffbuf) + 512 + strlen(history_msg->cm_fields['M']) + strlen(diffbuf) + 1024 ); - ptr = bmstrcasestr(history_msg->cm_fields['M'], endary); + ptr = bmstrcasestr(history_msg->cm_fields['M'], prefixed_boundary); if (ptr != NULL) { + char *the_rest_of_it = strdup(ptr); + char uuid[32]; + char memo[512]; + char encoded_memo[768]; + generate_uuid(uuid); + snprintf(memo, sizeof memo, "%s|%ld|%s|%s", + uuid, + time(NULL), + CCC->user.fullname, + config.c_nodename + /* no longer logging CCC->cs_inet_email */ + ); + CtdlEncodeBase64(encoded_memo, memo, strlen(memo), 0); sprintf(ptr, "--%s\n" "Content-type: text/plain\n" - "From: %s <%s>\n" + "Content-Disposition: inline; filename=\"%s\"\n" + "Content-Transfer-Encoding: 8bit\n" "\n" "%s\n" - "--%s--\n" + "%s" , boundary, - CCC->user.fullname, - CCC->cs_inet_email, + encoded_memo, diffbuf, - boundary + the_rest_of_it ); + free(the_rest_of_it); } history_msg->cm_fields['T'] = realloc(history_msg->cm_fields['T'], 32); @@ -277,6 +324,330 @@ int wiki_upload_beforesave(struct CtdlMessage *msg) { } +/* + * MIME Parser callback for wiki_history() + * + * The "filename" field will contain a memo field. All we have to do is decode + * the base64 and output it. The data is already in a delimited format suitable + * for our client protocol. + */ +void wiki_history_callback(char *name, char *filename, char *partnum, char *disp, + void *content, char *cbtype, char *cbcharset, size_t length, + char *encoding, char *cbid, void *cbuserdata) +{ + char memo[1024]; + + CtdlDecodeBase64(memo, filename, strlen(filename)); + cprintf("%s\n", memo); +} + + +/* + * Fetch a list of revisions for a particular wiki page + */ +void wiki_history(char *pagename) { + int r; + char history_page_name[270]; + long msgnum; + struct CtdlMessage *msg; + + r = CtdlDoIHavePermissionToReadMessagesInThisRoom(); + if (r != om_ok) { + if (r == om_not_logged_in) { + cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN); + } + else { + cprintf("%d An unknown error has occurred.\n", ERROR); + } + return; + } + + snprintf(history_page_name, sizeof history_page_name, "%s_HISTORY_", pagename); + msgnum = CtdlLocateMessageByEuid(history_page_name, &CC->room); + if (msgnum > 0L) { + msg = CtdlFetchMessage(msgnum, 1); + } + else { + msg = NULL; + } + + if ((msg != NULL) && (msg->cm_fields['M'] == NULL)) { + CtdlFreeMessage(msg); + msg = NULL; + } + + if (msg == NULL) { + cprintf("%d Revision history for '%s' was not found.\n", ERROR+MESSAGE_NOT_FOUND, pagename); + return; + } + + + cprintf("%d Revision history for '%s'\n", LISTING_FOLLOWS, pagename); + mime_parser(msg->cm_fields['M'], NULL, *wiki_history_callback, NULL, NULL, NULL, 0); + cprintf("000\n"); + + CtdlFreeMessage(msg); + return; +} + +/* + * MIME Parser callback for wiki_rev() + * + * The "filename" field will contain a memo field, which includes (among other things) + * the uuid of this revision. After we hit the desired revision, we stop processing. + * + * The "content" filed will contain "diff" output suitable for applying via "patch" + * to our temporary file. + */ +void wiki_rev_callback(char *name, char *filename, char *partnum, char *disp, + void *content, char *cbtype, char *cbcharset, size_t length, + char *encoding, char *cbid, void *cbuserdata) +{ + struct HistoryEraserCallBackData *hecbd = (struct HistoryEraserCallBackData *)cbuserdata; + char memo[1024]; + char this_rev[256]; + FILE *fp; + char *ptr = NULL; + char buf[1024]; + + /* Did a previous callback already indicate that we've reached our target uuid? + * If so, don't process anything else. + */ + if (hecbd->done) { + return; + } + + CtdlDecodeBase64(memo, filename, strlen(filename)); + extract_token(this_rev, memo, 0, '|', sizeof this_rev); + CtdlLogPrintf(CTDL_DEBUG, "callback found rev: %s\n", this_rev); + + /* Perform the patch */ + fp = popen("patch -f -s -p0 -r /dev/null >/dev/null 2>/dev/null", "w"); + if (fp) { + /* Replace the filenames in the patch with the tempfilename we're actually tweaking */ + fprintf(fp, "--- %s\n", hecbd->tempfilename); + fprintf(fp, "+++ %s\n", hecbd->tempfilename); + + ptr = (char *)content; + int linenum = 0; + do { + ++linenum; + ptr = memreadline(ptr, buf, sizeof buf); + if (*ptr != 0) { + if (linenum <= 2) { + /* skip the first two lines; they contain bogus filenames */ + } + else { + fprintf(fp, "%s\n", buf); + } + } + } while ((*ptr != 0) && (ptr < ((char*)content + length))); + if (pclose(fp) != 0) { + CtdlLogPrintf(CTDL_ERR, "pclose() returned an error - patch failed\n"); + } + } + + if (!strcasecmp(this_rev, hecbd->stop_when)) { + /* Found our target rev. Tell any subsequent callbacks to suppress processing. */ + CtdlLogPrintf(CTDL_DEBUG, "Target revision has been reached -- stop patching.\n"); + hecbd->done = 1; + } +} + + +/* + * Fetch a specific revision of a wiki page. The "operation" string may be set to "fetch" in order + * to simply fetch the desired revision and store it in a temporary location for viewing, or "revert" + * to revert the currently active page to that revision. + */ +void wiki_rev(char *pagename, char *rev, char *operation) +{ + int r; + char history_page_name[270]; + long msgnum; + char temp[PATH_MAX]; + char timestamp[64]; + struct CtdlMessage *msg; + FILE *fp; + struct HistoryEraserCallBackData hecbd; + long len = 0L; + int rv; + + r = CtdlDoIHavePermissionToReadMessagesInThisRoom(); + if (r != om_ok) { + if (r == om_not_logged_in) { + cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN); + } + else { + cprintf("%d An unknown error has occurred.\n", ERROR); + } + return; + } + + /* Begin by fetching the current version of the page. We're going to patch + * backwards through the diffs until we get the one we want. + */ + msgnum = CtdlLocateMessageByEuid(pagename, &CC->room); + if (msgnum > 0L) { + msg = CtdlFetchMessage(msgnum, 1); + } + else { + msg = NULL; + } + + if ((msg != NULL) && (msg->cm_fields['M'] == NULL)) { + CtdlFreeMessage(msg); + msg = NULL; + } + + if (msg == NULL) { + cprintf("%d Page '%s' was not found.\n", ERROR+MESSAGE_NOT_FOUND, pagename); + return; + } + + /* Output it to a temporary file */ + + CtdlMakeTempFileName(temp, sizeof temp); + fp = fopen(temp, "w"); + if (fp != NULL) { + r = fwrite(msg->cm_fields['M'], strlen(msg->cm_fields['M']), 1, fp); + fclose(fp); + } + else { + CtdlLogPrintf(CTDL_ALERT, "Cannot open %s: %s\n", temp, strerror(errno)); + } + CtdlFreeMessage(msg); + + /* Get the revision history */ + + snprintf(history_page_name, sizeof history_page_name, "%s_HISTORY_", pagename); + msgnum = CtdlLocateMessageByEuid(history_page_name, &CC->room); + if (msgnum > 0L) { + msg = CtdlFetchMessage(msgnum, 1); + } + else { + msg = NULL; + } + + if ((msg != NULL) && (msg->cm_fields['M'] == NULL)) { + CtdlFreeMessage(msg); + msg = NULL; + } + + if (msg == NULL) { + cprintf("%d Revision history for '%s' was not found.\n", ERROR+MESSAGE_NOT_FOUND, pagename); + return; + } + + /* Start patching backwards (newest to oldest) through the revision history until we + * hit the revision uuid requested by the user. (The callback will perform each one.) + */ + + memset(&hecbd, 0, sizeof(struct HistoryEraserCallBackData)); + hecbd.tempfilename = temp; + hecbd.stop_when = rev; + + mime_parser(msg->cm_fields['M'], NULL, *wiki_rev_callback, NULL, NULL, (void *)&hecbd, 0); + CtdlFreeMessage(msg); + + /* Were we successful? */ + if (hecbd.done == 0) { + cprintf("%d Revision '%s' of page '%s' was not found.\n", + ERROR + MESSAGE_NOT_FOUND, rev, pagename + ); + } + + /* We have the desired revision on disk. Now do something with it. */ + + else if ( (!strcasecmp(operation, "fetch")) || (!strcasecmp(operation, "revert")) ) { + msg = malloc(sizeof(struct CtdlMessage)); + memset(msg, 0, sizeof(struct CtdlMessage)); + msg->cm_magic = CTDLMESSAGE_MAGIC; + msg->cm_anon_type = MES_NORMAL; + msg->cm_format_type = FMT_RFC822; + fp = fopen(temp, "r"); + if (fp) { + fseek(fp, 0L, SEEK_END); + len = ftell(fp); + fseek(fp, 0L, SEEK_SET); + msg->cm_fields['M'] = malloc(len + 1); + rv = fread(msg->cm_fields['M'], len, 1, fp); + CtdlLogPrintf(CTDL_DEBUG, "did %d blocks of %ld bytes\n", rv, len); + msg->cm_fields['M'][len] = 0; + fclose(fp); + } + if (len <= 0) { + msgnum = (-1L); + } + else if (!strcasecmp(operation, "fetch")) { + msg->cm_fields['A'] = strdup("Citadel"); + CtdlCreateRoom(wwm, 5, "", 0, 1, 1, VIEW_BBS); /* Not an error if already exists */ + msgnum = CtdlSubmitMsg(msg, NULL, wwm, 0); /* Store the revision here */ + } + else if (!strcasecmp(operation, "revert")) { + snprintf(timestamp, sizeof timestamp, "%ld", time(NULL)); + msg->cm_fields['T'] = strdup(timestamp); + msg->cm_fields['A'] = strdup(CC->user.fullname); + msg->cm_fields['F'] = strdup(CC->cs_inet_email); + msg->cm_fields['O'] = strdup(CC->room.QRname); + msg->cm_fields['N'] = strdup(NODENAME); + msg->cm_fields['E'] = strdup(pagename); + msgnum = CtdlSubmitMsg(msg, NULL, "", 0); /* Replace the current revision */ + } + else { + /* Theoretically it is impossible to get here, but throw an error anyway */ + msgnum = (-1L); + } + CtdlFreeMessage(msg); + if (msgnum >= 0L) { + cprintf("%d %ld\n", CIT_OK, msgnum); /* Give the client a msgnum */ + } + else { + cprintf("%d Error %ld has occurred.\n", ERROR+INTERNAL_ERROR, msgnum); + } + } + + /* We did all this work for nothing. Express anguish to the caller. */ + else { + cprintf("%d An unknown operation was requested.\n", ERROR+CMD_NOT_SUPPORTED); + } + + unlink(temp); + return; +} + + + +/* + * commands related to wiki management + */ +void cmd_wiki(char *argbuf) { + char subcmd[32]; + char pagename[256]; + char rev[128]; + char operation[16]; + + extract_token(subcmd, argbuf, 0, '|', sizeof subcmd); + + if (!strcasecmp(subcmd, "history")) { + extract_token(pagename, argbuf, 1, '|', sizeof pagename); + wiki_history(pagename); + return; + } + + if (!strcasecmp(subcmd, "rev")) { + extract_token(pagename, argbuf, 1, '|', sizeof pagename); + extract_token(rev, argbuf, 2, '|', sizeof rev); + extract_token(operation, argbuf, 3, '|', sizeof operation); + wiki_rev(pagename, rev, operation); + return; + } + + cprintf("%d Invalid subcommand\n", ERROR + CMD_NOT_SUPPORTED); +} + + + /* * Module initialization */ @@ -285,8 +656,9 @@ CTDL_MODULE_INIT(wiki) if (!threading) { CtdlRegisterMessageHook(wiki_upload_beforesave, EVT_BEFORESAVE); + CtdlRegisterProtoHook(cmd_wiki, "WIKI", "Commands related to Wiki management"); } /* return our Subversion id for the Log */ - return "$Id$"; + return "wiki"; }