+ }
+
+ syslog(LOG_DEBUG, "diff length is "SIZE_T_FMT" bytes", diffbuf_len);
+
+ unlink(diff_old_filename);
+ unlink(diff_new_filename);
+ unlink(diff_out_filename);
+
+ /* Determine whether this was a bogus (empty) edit */
+ if ((diffbuf_len = 0) && (diffbuf != NULL)) {
+ free(diffbuf);
+ diffbuf = NULL;
+ }
+ if (diffbuf == NULL) {
+ return(1); /* No changes at all? Abandon the post entirely! */
+ }
+
+ /* Now look for the existing edit history */
+
+ history_msgnum = CtdlLocateMessageByEuid(history_page, &CCC->room);
+ history_msg = NULL;
+ if (history_msgnum > 0L) {
+ history_msg = CtdlFetchMessage(history_msgnum, 1);
+ }
+
+ /* Create a new history message if necessary */
+ if (history_msg == NULL) {
+ char *buf;
+ long len;
+
+ history_msg = malloc(sizeof(struct CtdlMessage));
+ memset(history_msg, 0, sizeof(struct CtdlMessage));
+ history_msg->cm_magic = CTDLMESSAGE_MAGIC;
+ history_msg->cm_anon_type = MES_NORMAL;
+ history_msg->cm_format_type = FMT_RFC822;
+ CM_SetField(history_msg, eAuthor, HKEY("Citadel"));
+ if (!IsEmptyStr(CCC->room.QRname)){
+ CM_SetField(history_msg, eRecipient, CCC->room.QRname, strlen(CCC->room.QRname));
+ }
+ CM_SetField(history_msg, eExclusiveID, history_page, history_page_len);
+ CM_SetField(history_msg, eMsgSubject, history_page, history_page_len);
+ CM_SetField(history_msg, eSuppressIdx, HKEY("1")); /* suppress full text indexing */
+ snprintf(boundary, sizeof boundary, "Citadel--Multipart--%04x--%08lx", getpid(), time(NULL));
+ buf = (char*) malloc(1024);
+ len = snprintf(buf, 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
+ );
+ CM_SetAsField(history_msg, eMesageText, &buf, len);
+ }
+
+ /* Update the history message (regardless of whether it's new or existing) */
+
+ /* 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.
+ */
+ CM_FlushField(history_msg, emessageId);
+
+ /* 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.
+ */
+ *boundary = '\0';
+
+ ptr = history_msg->cm_fields[eMesageText];
+ do {
+ ptr = memreadline(ptr, buf, sizeof buf);
+ if (*ptr != 0) {
+ striplt(buf);
+ if (!IsEmptyStr(buf) && (!strncasecmp(buf, "Content-type:", 13))) {
+ if (
+ (bmstrcasestr(buf, "multipart") != NULL)
+ && (bmstrcasestr(buf, "boundary=") != NULL)
+ ) {
+ safestrncpy(boundary, bmstrcasestr(buf, "\""), sizeof boundary);
+ char *qu;
+ qu = strchr(boundary, '\"');
+ if (qu) {
+ strcpy(boundary, ++qu);
+ }
+ qu = strchr(boundary, '\"');
+ if (qu) {
+ *qu = 0;
+ }
+ }
+ }
+ }
+ } while ( (IsEmptyStr(boundary)) && (*ptr != 0) );
+
+ /*
+ * Now look for the first boundary. That is where we need to insert our fun.
+ */
+ if (!IsEmptyStr(boundary)) {
+ char *MsgText;
+ long MsgTextLen;
+ time_t Now = time(NULL);
+
+ snprintf(prefixed_boundary, sizeof(prefixed_boundary), "--%s", boundary);
+
+ CM_GetAsField(history_msg, eMesageText, &MsgText, &MsgTextLen);
+
+ ptr = bmstrcasestr(MsgText, prefixed_boundary);
+ if (ptr != NULL) {
+ StrBuf *NewMsgText;
+ char uuid[64];
+ char memo[512];
+ long memolen;
+ char encoded_memo[1024];
+
+ NewMsgText = NewStrBufPlain(NULL, MsgTextLen + diffbuf_len + 1024);
+
+ generate_uuid(uuid);
+ memolen = snprintf(memo, sizeof(memo), "%s|%ld|%s|%s",
+ uuid,
+ Now,
+ CCC->user.fullname,
+ CtdlGetConfigStr("c_nodename"));
+
+ memolen = CtdlEncodeBase64(encoded_memo, memo, memolen, 0);
+
+ StrBufAppendBufPlain(NewMsgText, HKEY("--"), 0);
+ StrBufAppendBufPlain(NewMsgText, boundary, -1, 0);
+ StrBufAppendBufPlain(
+ NewMsgText,
+ HKEY("\n"
+ "Content-type: text/plain\n"
+ "Content-Disposition: inline; filename=\""), 0);
+
+ StrBufAppendBufPlain(NewMsgText, encoded_memo, memolen, 0);
+
+ StrBufAppendBufPlain(
+ NewMsgText,
+ HKEY("\"\n"
+ "Content-Transfer-Encoding: 8bit\n"
+ "\n"), 0);
+
+ StrBufAppendBufPlain(NewMsgText, diffbuf, diffbuf_len, 0);
+ StrBufAppendBufPlain(NewMsgText, HKEY("\n"), 0);
+
+ StrBufAppendBufPlain(NewMsgText, ptr, MsgTextLen - (ptr - MsgText), 0);
+ free(MsgText);
+ CM_SetAsFieldSB(history_msg, eMesageText, &NewMsgText);
+ }
+ else
+ {
+ CM_SetAsField(history_msg, eMesageText, &MsgText, MsgTextLen);
+ }
+
+ CM_SetFieldLONG(history_msg, eTimestamp, Now);
+
+ CtdlSubmitMsg(history_msg, NULL, "");
+ }
+ else {
+ syslog(LOG_ALERT, "Empty boundary string in history message. No history!\n");
+ }
+
+ free(diffbuf);
+ CM_Free(history_msg);
+ return(0);
+}
+
+
+/*
+ * 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) && CM_IsEmpty(msg, eMesageText)) {
+ CM_Free(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(CM_RANGE(msg, eMesageText), *wiki_history_callback, NULL, NULL, NULL, 0);
+ cprintf("000\n");
+
+ CM_Free(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);
+ striplt(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) {
+ syslog(LOG_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. */
+ syslog(LOG_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)
+{
+ struct CitContext *CCC = CC;
+ int r;
+ char history_page_name[270];
+ long msgnum;
+ char temp[PATH_MAX];
+ 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;
+ }
+
+ if (!strcasecmp(operation, "revert")) {
+ r = CtdlDoIHavePermissionToPostInThisRoom(temp, sizeof temp, NULL, POST_LOGGED_IN, 0);
+ if (r != 0) {
+ cprintf("%d %s\n", r, temp);
+ 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, &CCC->room);
+ if (msgnum > 0L) {
+ msg = CtdlFetchMessage(msgnum, 1);
+ }
+ else {
+ msg = NULL;
+ }
+
+ if ((msg != NULL) && CM_IsEmpty(msg, eMesageText)) {
+ CM_Free(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[eMesageText], msg->cm_lengths[eMesageText], 1, fp);