formatting
[citadel.git] / citadel / modules / migrate / serv_migrate.c
1 // This module dumps and/or loads the Citadel database in XML format.
2 //
3 // Copyright (c) 1987-2021 by the citadel.org team
4 //
5 // This program is open source software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License version 3.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // Explanation of <progress> tags:
14 //
15 // 0%           started
16 // 2%           finished exporting configuration
17 // 7%           finished exporting users
18 // 12%          finished exporting openids
19 // 17%          finished exporting rooms
20 // 18%          finished exporting floors
21 // 25%          finished exporting visits
22 // 26-99%       exporting messages
23 // 100%         finished exporting messages
24 //
25 // These tags are inserted into the XML stream to give the reader an approximation of its progress.
26
27 #include "sysdep.h"
28 #include <stdlib.h>
29 #include <unistd.h>
30 #include <stdio.h>
31 #include <fcntl.h>
32 #include <signal.h>
33 #include <pwd.h>
34 #include <errno.h>
35 #include <sys/types.h>
36 #include <time.h>
37 #include <sys/wait.h>
38 #include <string.h>
39 #include <ctype.h>
40 #include <limits.h>
41 #include <expat.h>
42 #include <libcitadel.h>
43 #include "citadel.h"
44 #include "server.h"
45 #include "citserver.h"
46 #include "support.h"
47 #include "config.h"
48 #include "database.h"
49 #include "msgbase.h"
50 #include "user_ops.h"
51 #include "euidindex.h"
52 #include "ctdl_module.h"
53
54 char migr_tempfilename1[PATH_MAX];
55 char migr_tempfilename2[PATH_MAX];
56 FILE *migr_global_message_list;
57 int total_msgs = 0;
58 char *ikey = NULL;                      // If we're importing a config key we store it here.
59
60 //*****************************************************************************
61 //*       Code which implements the export appears in this section            *
62 //*****************************************************************************
63
64 // Output a string to the client with these characters escaped:  & < > " '
65 void xml_strout(char *str) {
66
67         char *c = str;
68
69         if (str == NULL) {
70                 return;
71         }
72
73         while (*c != 0) {
74                 if (*c == '\"') {
75                         client_write(HKEY("&quot;"));
76                 }
77                 else if (*c == '\'') {
78                         client_write(HKEY("&apos;"));
79                 }
80                 else if (*c == '<') {
81                         client_write(HKEY("&lt;"));
82                 }
83                 else if (*c == '>') {
84                         client_write(HKEY("&gt;"));
85                 }
86                 else if (*c == '&') {
87                         client_write(HKEY("&amp;"));
88                 }
89                 else {
90                         client_write(c, 1);
91                 }
92                 ++c;
93         }
94 }
95
96
97 // Export a user record as XML
98 void migr_export_users_backend(char *username, void *data) {
99
100         struct ctdluser u;
101         if (CtdlGetUser(&u, username) != 0) {
102                 return;
103         }
104
105         client_write(HKEY("<user>\n"));
106         cprintf("<u_version>%d</u_version>\n", u.version);
107         cprintf("<u_uid>%ld</u_uid>\n", (long)u.uid);
108         client_write(HKEY("<u_password>"));     xml_strout(u.password);         client_write(HKEY("</u_password>\n"));
109         cprintf("<u_flags>%u</u_flags>\n", u.flags);
110         cprintf("<u_timescalled>%ld</u_timescalled>\n", u.timescalled);
111         cprintf("<u_posted>%ld</u_posted>\n", u.posted);
112         cprintf("<u_axlevel>%d</u_axlevel>\n", u.axlevel);
113         cprintf("<u_usernum>%ld</u_usernum>\n", u.usernum);
114         cprintf("<u_lastcall>%ld</u_lastcall>\n", (long)u.lastcall);
115         cprintf("<u_USuserpurge>%d</u_USuserpurge>\n", u.USuserpurge);
116         client_write(HKEY("<u_fullname>"));     xml_strout(u.fullname);         client_write(HKEY("</u_fullname>\n"));
117         cprintf("<u_msgnum_bio>%ld</u_msgnum_bio>\n", u.msgnum_bio);
118         cprintf("<u_msgnum_pic>%ld</u_msgnum_pic>\n", u.msgnum_pic);
119         cprintf("<u_emailaddrs>%s</u_emailaddrs>\n", u.emailaddrs);
120         cprintf("<u_msgnum_inboxrules>%ld</u_msgnum_inboxrules>\n", u.msgnum_inboxrules);
121         cprintf("<u_lastproc_inboxrules>%ld</u_lastproc_inboxrules>\n", u.lastproc_inboxrules);
122         client_write(HKEY("</user>\n"));
123 }
124
125
126 void migr_export_users(void) {
127         ForEachUser(migr_export_users_backend, NULL);
128 }
129
130
131 void migr_export_room_msg(long msgnum, void *userdata) {
132         static int count = 0;
133
134         cprintf("%ld,", msgnum);
135         if (++count%10==0) {
136                 cprintf("\n");
137         }
138         fprintf(migr_global_message_list, "%ld\n", msgnum);
139 }
140
141
142 void migr_export_rooms_backend(struct ctdlroom *buf, void *data) {
143         client_write(HKEY("<room>\n"));
144         client_write(HKEY("<QRname>"));
145         xml_strout(buf->QRname);
146         client_write(HKEY("</QRname>\n"));
147         client_write(HKEY("<QRpasswd>"));
148         xml_strout(buf->QRpasswd);
149         client_write(HKEY("</QRpasswd>\n"));
150         cprintf("<QRroomaide>%ld</QRroomaide>\n", buf->QRroomaide);
151         cprintf("<QRhighest>%ld</QRhighest>\n", buf->QRhighest);
152         cprintf("<QRgen>%ld</QRgen>\n", (long)buf->QRgen);
153         cprintf("<QRflags>%u</QRflags>\n", buf->QRflags);
154         if (buf->QRflags & QR_DIRECTORY) {
155                 client_write(HKEY("<QRdirname>"));
156                 xml_strout(buf->QRdirname);
157                 client_write(HKEY("</QRdirname>\n"));
158         }
159         cprintf("<QRfloor>%d</QRfloor>\n", buf->QRfloor);
160         cprintf("<QRmtime>%ld</QRmtime>\n", (long)buf->QRmtime);
161         cprintf("<QRexpire_mode>%d</QRexpire_mode>\n", buf->QRep.expire_mode);
162         cprintf("<QRexpire_value>%d</QRexpire_value>\n", buf->QRep.expire_value);
163         cprintf("<QRnumber>%ld</QRnumber>\n", buf->QRnumber);
164         cprintf("<QRorder>%d</QRorder>\n", buf->QRorder);
165         cprintf("<QRflags2>%u</QRflags2>\n", buf->QRflags2);
166         cprintf("<QRdefaultview>%d</QRdefaultview>\n", buf->QRdefaultview);
167         cprintf("<msgnum_info>%ld</msgnum_info>\n", buf->msgnum_info);
168         cprintf("<msgnum_pic>%ld</msgnum_pic>\n", buf->msgnum_pic);
169         client_write(HKEY("</room>\n"));
170
171         // message list goes inside this tag
172         CtdlGetRoom(&CC->room, buf->QRname);
173         client_write(HKEY("<room_messages>"));
174         client_write(HKEY("<FRname>"));
175         xml_strout(buf->QRname);                        // buf->QRname rather than CC->room.QRname to guarantee consistency
176         client_write(HKEY("</FRname>\n"));
177         client_write(HKEY("<FRmsglist>\n"));
178         CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, migr_export_room_msg, NULL);
179         client_write(HKEY("</FRmsglist>\n"));
180         client_write(HKEY("</room_messages>\n"));
181 }
182
183
184 void migr_export_rooms(void) {
185         char cmd[SIZ];
186         migr_global_message_list = fopen(migr_tempfilename1, "w");
187         if (migr_global_message_list != NULL) {
188                 CtdlForEachRoom(migr_export_rooms_backend, NULL);
189                 fclose(migr_global_message_list);
190         }
191
192         // Process the 'global' message list.  (Sort it and remove dups.
193         // Dups are ok because a message may be in more than one room, but
194         // this will be handled by exporting the reference count, not by
195         // exporting the message multiple times.)
196         snprintf(cmd, sizeof cmd, "sort -n <%s >%s", migr_tempfilename1, migr_tempfilename2);
197         if (system(cmd) != 0) {
198                 syslog(LOG_ERR, "migrate: error %d", errno);
199         }
200         snprintf(cmd, sizeof cmd, "uniq <%s >%s", migr_tempfilename2, migr_tempfilename1);
201         if (system(cmd) != 0) {
202                 syslog(LOG_ERR, "migrate: error %d", errno);
203         }
204
205         snprintf(cmd, sizeof cmd, "wc -l %s", migr_tempfilename1);
206         FILE *fp = popen(cmd, "r");
207         if (fp) {
208                 fgets(cmd, sizeof cmd, fp);
209                 pclose(fp);
210                 total_msgs = atoi(cmd);
211         }
212         else {
213                 total_msgs = 1; // any nonzero just to keep it from barfing
214         }
215         syslog(LOG_DEBUG, "migrate: total messages to be exported: %d", total_msgs);
216 }
217
218
219 void migr_export_floors(void) {
220         struct floor qfbuf, *buf;
221         int i;
222
223         for (i=0; i < MAXFLOORS; ++i) {
224                 client_write(HKEY("<floor>\n"));
225                 cprintf("<f_num>%d</f_num>\n", i);
226                 CtdlGetFloor(&qfbuf, i);
227                 buf = &qfbuf;
228                 cprintf("<f_flags>%u</f_flags>\n", buf->f_flags);
229                 client_write(HKEY("<f_name>")); xml_strout(buf->f_name); client_write(HKEY("</f_name>\n"));
230                 cprintf("<f_ref_count>%d</f_ref_count>\n", buf->f_ref_count);
231                 cprintf("<f_ep_expire_mode>%d</f_ep_expire_mode>\n", buf->f_ep.expire_mode);
232                 cprintf("<f_ep_expire_value>%d</f_ep_expire_value>\n", buf->f_ep.expire_value);
233                 client_write(HKEY("</floor>\n"));
234         }
235 }
236
237
238 // Return nonzero if the supplied string contains only characters which are valid in a sequence set.
239 int is_sequence_set(char *s) {
240         if (!s) return(0);
241
242         char *c = s;
243         char ch;
244         while (ch = *c++, ch) {
245                 if (!strchr("0123456789*,:", ch)) {
246                         return(0);
247                 }
248         }
249         return(1);
250 }
251
252
253 // Traverse the visits file...
254 void migr_export_visits(void) {
255         visit vbuf;
256         struct cdbdata *cdbv;
257
258         cdb_rewind(CDB_VISIT);
259
260         while (cdbv = cdb_next_item(CDB_VISIT), cdbv != NULL) {
261                 memset(&vbuf, 0, sizeof(visit));
262                 memcpy(&vbuf, cdbv->ptr,
263                        ((cdbv->len > sizeof(visit)) ?
264                         sizeof(visit) : cdbv->len));
265                 cdb_free(cdbv);
266
267                 client_write(HKEY("<visit>\n"));
268                 cprintf("<v_roomnum>%ld</v_roomnum>\n", vbuf.v_roomnum);
269                 cprintf("<v_roomgen>%ld</v_roomgen>\n", vbuf.v_roomgen);
270                 cprintf("<v_usernum>%ld</v_usernum>\n", vbuf.v_usernum);
271
272                 client_write(HKEY("<v_seen>"));
273                 if ( (!IsEmptyStr(vbuf.v_seen)) && (is_sequence_set(vbuf.v_seen)) ) {
274                         xml_strout(vbuf.v_seen);
275                 }
276                 else {
277                         cprintf("%ld", vbuf.v_lastseen);
278                 }
279                 client_write(HKEY("</v_seen>"));
280
281                 if ( (!IsEmptyStr(vbuf.v_answered)) && (is_sequence_set(vbuf.v_answered)) ) {
282                         client_write(HKEY("<v_answered>"));
283                         xml_strout(vbuf.v_answered);
284                         client_write(HKEY("</v_answered>\n"));
285                 }
286
287                 cprintf("<v_flags>%u</v_flags>\n", vbuf.v_flags);
288                 cprintf("<v_view>%d</v_view>\n", vbuf.v_view);
289                 client_write(HKEY("</visit>\n"));
290         }
291 }
292
293
294 void migr_export_message(long msgnum) {
295         struct MetaData smi;
296         struct CtdlMessage *msg;
297         struct ser_ret smr;
298         long bytes_written = 0;
299         long this_block = 0;
300
301         // We can use a static buffer here because there will never be more than
302         // one of this operation happening at any given time, and it's really best
303         // to just keep it allocated once instead of torturing malloc/free.
304         // Call this function with msgnum "-1" to free the buffer when finished.
305
306         static int encoded_alloc = 0;
307         static char *encoded_msg = NULL;
308
309         if (msgnum < 0) {
310                 if ((encoded_alloc == 0) && (encoded_msg != NULL)) {
311                         free(encoded_msg);
312                         encoded_alloc = 0;
313                         encoded_msg = NULL;
314                 }
315                 return;
316         }
317
318         // Ok, here we go ...
319
320         msg = CtdlFetchMessage(msgnum, 1);
321         if (msg == NULL) return;                                // fail silently
322
323         client_write(HKEY("<message>\n"));
324         GetMetaData(&smi, msgnum);
325         cprintf("<msg_msgnum>%ld</msg_msgnum>\n", msgnum);
326         cprintf("<msg_meta_refcount>%d</msg_meta_refcount>\n", smi.meta_refcount);
327         cprintf("<msg_meta_rfc822_length>%ld</msg_meta_rfc822_length>\n", smi.meta_rfc822_length);
328         client_write(HKEY("<msg_meta_content_type>")); xml_strout(smi.meta_content_type); client_write(HKEY("</msg_meta_content_type>\n"));
329
330         client_write(HKEY("<msg_text>"));
331         CtdlSerializeMessage(&smr, msg);
332         CM_Free(msg);
333
334         // Predict the buffer size we need.  Expand the buffer if necessary.
335         int encoded_len = smr.len * 15 / 10 ;
336         if (encoded_len > encoded_alloc) {
337                 encoded_alloc = encoded_len;
338                 encoded_msg = realloc(encoded_msg, encoded_alloc);
339         }
340
341         if (encoded_msg == NULL) {
342                 // Questionable hack that hopes it'll work next time and we only lose one message
343                 encoded_alloc = 0;
344         }
345         else {
346                 // Once we do the encoding we know the exact size
347                 encoded_len = CtdlEncodeBase64(encoded_msg, (char *)smr.ser, smr.len, 1);
348
349                 // Careful now.  If the message is gargantuan, trying to write multiple gigamegs in one
350                 // big write operation can make our transport unhappy.  So we'll chunk it up 10 KB at a time.
351                 bytes_written = 0;
352                 while ( (bytes_written < encoded_len) && (!server_shutting_down) ) {
353                         this_block = encoded_len - bytes_written;
354                         if (this_block > 10240) {
355                                 this_block = 10240;
356                         }
357                         client_write(&encoded_msg[bytes_written], this_block);
358                         bytes_written += this_block;
359                 }
360         }
361
362         free(smr.ser);
363
364         client_write(HKEY("</msg_text>\n"));
365         client_write(HKEY("</message>\n"));
366 }
367
368
369 void migr_export_openids(void) {
370         struct cdbdata *cdboi;
371         long usernum;
372         char url[512];
373
374         cdb_rewind(CDB_EXTAUTH);
375         while (cdboi = cdb_next_item(CDB_EXTAUTH), cdboi != NULL) {
376                 if (cdboi->len > sizeof(long)) {
377                         client_write(HKEY("<openid>\n"));
378                         memcpy(&usernum, cdboi->ptr, sizeof(long));
379                         snprintf(url, sizeof url, "%s", (cdboi->ptr)+sizeof(long) );
380                         client_write(HKEY("<oid_url>"));
381                         xml_strout(url);
382                         client_write(HKEY("</oid_url>\n"));
383                         cprintf("<oid_usernum>%ld</oid_usernum>\n", usernum);
384                         client_write(HKEY("</openid>\n"));
385                 }
386                 cdb_free(cdboi);
387         }
388 }
389
390
391 void migr_export_configs(void) {
392         struct cdbdata *cdbcfg;
393         int keylen = 0;
394         char *key = NULL;
395         char *value = NULL;
396
397         cdb_rewind(CDB_CONFIG);
398         while (cdbcfg = cdb_next_item(CDB_CONFIG), cdbcfg != NULL) {
399
400                 keylen = strlen(cdbcfg->ptr);
401                 key = cdbcfg->ptr;
402                 value = cdbcfg->ptr + keylen + 1;
403
404                 client_write("<config key=\"", 13);
405                 xml_strout(key);
406                 client_write("\">", 2);
407                 xml_strout(value);
408                 client_write("</config>\n", 10);
409                 cdb_free(cdbcfg);
410         }
411 }
412
413
414 void migr_export_messages(void) {
415         char buf[SIZ];
416         long msgnum;
417         int count = 0;
418         int progress = 0;
419         int prev_progress = 0;
420         CitContext *Ctx;
421
422         Ctx = CC;
423         migr_global_message_list = fopen(migr_tempfilename1, "r");
424         if (migr_global_message_list != NULL) {
425                 syslog(LOG_INFO, "migrate: opened %s", migr_tempfilename1);
426                 while ((Ctx->kill_me == 0) && 
427                        (fgets(buf, sizeof(buf), migr_global_message_list) != NULL)) {
428                         msgnum = atol(buf);
429                         if (msgnum > 0L) {
430                                 migr_export_message(msgnum);
431                                 ++count;
432                         }
433                         progress = (count * 74 / total_msgs) + 25 ;
434                         if ((progress > prev_progress) && (progress < 100)) {
435                                 cprintf("<progress>%d</progress>\n", progress);
436                         }
437                         prev_progress = progress;
438                 }
439                 fclose(migr_global_message_list);
440         }
441         if (Ctx->kill_me == 0) {
442                 syslog(LOG_INFO, "migrate: exported %d messages.", count);
443         }
444         else {
445                 syslog(LOG_ERR, "migrate: export aborted due to client disconnect!");
446         }
447
448         migr_export_message(-1L);       // This frees the encoding buffer
449 }
450
451
452 void migr_do_export(void) {
453         CitContext *Ctx;
454
455         Ctx = CC;
456         cprintf("%d Exporting all Citadel databases.\n", LISTING_FOLLOWS);
457         Ctx->dont_term = 1;
458
459         client_write(HKEY("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"));
460         client_write(HKEY("<citadel_migrate_data>\n"));
461         cprintf("<version>%d</version>\n", REV_LEVEL);
462         cprintf("<progress>%d</progress>\n", 0);
463
464         // export the configuration database
465         migr_export_configs();
466         cprintf("<progress>%d</progress>\n", 2);
467         
468         if (Ctx->kill_me == 0)  migr_export_users();
469         cprintf("<progress>%d</progress>\n", 7);
470         if (Ctx->kill_me == 0)  migr_export_openids();
471         cprintf("<progress>%d</progress>\n", 12);
472         if (Ctx->kill_me == 0)  migr_export_rooms();
473         cprintf("<progress>%d</progress>\n", 17);
474         if (Ctx->kill_me == 0)  migr_export_floors();
475         cprintf("<progress>%d</progress>\n", 18);
476         if (Ctx->kill_me == 0)  migr_export_visits();
477         cprintf("<progress>%d</progress>\n", 25);
478         if (Ctx->kill_me == 0)  migr_export_messages();
479         cprintf("<progress>%d</progress>\n", 100);
480         client_write(HKEY("</citadel_migrate_data>\n"));
481         client_write(HKEY("000\n"));
482         Ctx->dont_term = 0;
483 }
484
485
486 //                              Import code
487 //   Here's the code that implements the import side.  It's going to end up
488 //       being one big loop with lots of global variables.  I don't care.
489 // You wouldn't run multiple concurrent imports anyway.  If this offends your
490 //  delicate sensibilities  then go rewrite it in Ruby on Rails or something.
491
492
493 int citadel_migrate_data = 0;           // Are we inside a <citadel_migrate_data> tag pair?
494 StrBuf *migr_chardata = NULL;
495 StrBuf *migr_MsgData = NULL;
496 struct ctdluser usbuf;
497 struct ctdlroom qrbuf;
498 char openid_url[512];
499 long openid_usernum = 0;
500 char FRname[ROOMNAMELEN];
501 struct floor flbuf;
502 int floornum = 0;
503 visit vbuf;
504 struct MetaData smi;
505 long import_msgnum = 0;
506
507 // This callback stores up the data which appears in between tags.
508 void migr_xml_chardata(void *data, const XML_Char *s, int len) {
509         StrBufAppendBufPlain(migr_chardata, s, len, 0);
510 }
511
512
513 void migr_xml_start(void *data, const char *el, const char **attr) {
514         int i;
515
516         //  *** GENERAL STUFF ***
517
518         // Reset chardata_len to zero and init buffer
519         FlushStrBuf(migr_chardata);
520         FlushStrBuf(migr_MsgData);
521
522         if (!strcasecmp(el, "citadel_migrate_data")) {
523                 ++citadel_migrate_data;
524
525                 // As soon as it looks like the input data is a genuine Citadel XML export,
526                 // whack the existing database on disk to make room for the new one.
527                 if (citadel_migrate_data == 1) {
528                         syslog(LOG_INFO, "migrate: erasing existing databases so we can receive the incoming import");
529                         for (i = 0; i < MAXCDB; ++i) {
530                                 cdb_trunc(i);
531                         }
532                 }
533                 return;
534         }
535
536         if (citadel_migrate_data != 1) {
537                 syslog(LOG_ERR, "migrate: out-of-sequence tag <%s> detected.  Warning: ODD-DATA!", el);
538                 return;
539         }
540
541         // When we begin receiving XML for one of these record types, clear out the associated
542         // buffer so we don't accidentally carry over any data from a previous record.
543         if (!strcasecmp(el, "user"))                    memset(&usbuf, 0, sizeof (struct ctdluser));
544         else if (!strcasecmp(el, "openid"))             memset(openid_url, 0, sizeof openid_url);
545         else if (!strcasecmp(el, "room"))               memset(&qrbuf, 0, sizeof (struct ctdlroom));
546         else if (!strcasecmp(el, "room_messages"))      memset(FRname, 0, sizeof FRname);
547         else if (!strcasecmp(el, "floor"))              memset(&flbuf, 0, sizeof (struct floor));
548         else if (!strcasecmp(el, "visit"))              memset(&vbuf, 0, sizeof (visit));
549
550         else if (!strcasecmp(el, "message")) {
551                 memset(&smi, 0, sizeof (struct MetaData));
552                 import_msgnum = 0;
553         }
554         else if (!strcasecmp(el, "config")) {
555                 if (ikey != NULL) {
556                         free(ikey);
557                         ikey = NULL;
558                 }
559                 while (*attr) {
560                         if (!strcasecmp(attr[0], "key")) {
561                                 ikey = strdup(attr[1]);
562                         }
563                         attr += 2;
564                 }
565         }
566
567 }
568
569
570 int migr_userrecord(void *data, const char *el) {
571         if (!strcasecmp(el, "u_version"))                       usbuf.version = atoi(ChrPtr(migr_chardata));
572         else if (!strcasecmp(el, "u_uid"))                      usbuf.uid = atol(ChrPtr(migr_chardata));
573         else if (!strcasecmp(el, "u_password"))                 safestrncpy(usbuf.password, ChrPtr(migr_chardata), sizeof usbuf.password);
574         else if (!strcasecmp(el, "u_flags"))                    usbuf.flags = atoi(ChrPtr(migr_chardata));
575         else if (!strcasecmp(el, "u_timescalled"))              usbuf.timescalled = atol(ChrPtr(migr_chardata));
576         else if (!strcasecmp(el, "u_posted"))                   usbuf.posted = atol(ChrPtr(migr_chardata));
577         else if (!strcasecmp(el, "u_axlevel"))                  usbuf.axlevel = atoi(ChrPtr(migr_chardata));
578         else if (!strcasecmp(el, "u_usernum"))                  usbuf.usernum = atol(ChrPtr(migr_chardata));
579         else if (!strcasecmp(el, "u_lastcall"))                 usbuf.lastcall = atol(ChrPtr(migr_chardata));
580         else if (!strcasecmp(el, "u_USuserpurge"))              usbuf.USuserpurge = atoi(ChrPtr(migr_chardata));
581         else if (!strcasecmp(el, "u_fullname"))                 safestrncpy(usbuf.fullname, ChrPtr(migr_chardata), sizeof usbuf.fullname);
582         else if (!strcasecmp(el, "u_msgnum_bio"))               usbuf.msgnum_bio = atol(ChrPtr(migr_chardata));
583         else if (!strcasecmp(el, "u_msgnum_pic"))               usbuf.msgnum_pic = atol(ChrPtr(migr_chardata));
584         else if (!strcasecmp(el, "u_emailaddrs"))               safestrncpy(usbuf.emailaddrs, ChrPtr(migr_chardata), sizeof usbuf.emailaddrs);
585         else if (!strcasecmp(el, "u_msgnum_inboxrules"))        usbuf.msgnum_inboxrules = atol(ChrPtr(migr_chardata));
586         else if (!strcasecmp(el, "u_lastproc_inboxrules"))      usbuf.lastproc_inboxrules = atol(ChrPtr(migr_chardata));
587         else return 0;
588         return 1;
589 }
590
591
592 int migr_roomrecord(void *data, const char *el) {
593         if (!strcasecmp(el, "QRname"))                          safestrncpy(qrbuf.QRname, ChrPtr(migr_chardata), sizeof qrbuf.QRname);
594         else if (!strcasecmp(el, "QRpasswd"))                   safestrncpy(qrbuf.QRpasswd, ChrPtr(migr_chardata), sizeof qrbuf.QRpasswd);
595         else if (!strcasecmp(el, "QRroomaide"))                 qrbuf.QRroomaide = atol(ChrPtr(migr_chardata));
596         else if (!strcasecmp(el, "QRhighest"))                  qrbuf.QRhighest = atol(ChrPtr(migr_chardata));
597         else if (!strcasecmp(el, "QRgen"))                      qrbuf.QRgen = atol(ChrPtr(migr_chardata));
598         else if (!strcasecmp(el, "QRflags"))                    qrbuf.QRflags = atoi(ChrPtr(migr_chardata));
599         else if (!strcasecmp(el, "QRdirname"))                  safestrncpy(qrbuf.QRdirname, ChrPtr(migr_chardata), sizeof qrbuf.QRdirname);
600         else if (!strcasecmp(el, "QRfloor"))                    qrbuf.QRfloor = atoi(ChrPtr(migr_chardata));
601         else if (!strcasecmp(el, "QRmtime"))                    qrbuf.QRmtime = atol(ChrPtr(migr_chardata));
602         else if (!strcasecmp(el, "QRexpire_mode"))              qrbuf.QRep.expire_mode = atoi(ChrPtr(migr_chardata));
603         else if (!strcasecmp(el, "QRexpire_value"))             qrbuf.QRep.expire_value = atoi(ChrPtr(migr_chardata));
604         else if (!strcasecmp(el, "QRnumber"))                   qrbuf.QRnumber = atol(ChrPtr(migr_chardata));
605         else if (!strcasecmp(el, "QRorder"))                    qrbuf.QRorder = atoi(ChrPtr(migr_chardata));
606         else if (!strcasecmp(el, "QRflags2"))                   qrbuf.QRflags2 = atoi(ChrPtr(migr_chardata));
607         else if (!strcasecmp(el, "QRdefaultview"))              qrbuf.QRdefaultview = atoi(ChrPtr(migr_chardata));
608         else if (!strcasecmp(el, "msgnum_info"))                qrbuf.msgnum_info = atol(ChrPtr(migr_chardata));
609         else if (!strcasecmp(el, "msgnum_pic"))                 qrbuf.msgnum_pic = atol(ChrPtr(migr_chardata));
610         else return 0;
611         return 1;
612 }
613
614
615 int migr_floorrecord(void *data, const char *el) {
616         if (!strcasecmp(el, "f_num"))                           floornum = atoi(ChrPtr(migr_chardata));
617         else if (!strcasecmp(el, "f_flags"))                    flbuf.f_flags = atoi(ChrPtr(migr_chardata));
618         else if (!strcasecmp(el, "f_name"))                     safestrncpy(flbuf.f_name, ChrPtr(migr_chardata), sizeof flbuf.f_name);
619         else if (!strcasecmp(el, "f_ref_count"))                flbuf.f_ref_count = atoi(ChrPtr(migr_chardata));
620         else if (!strcasecmp(el, "f_ep_expire_mode"))           flbuf.f_ep.expire_mode = atoi(ChrPtr(migr_chardata));
621         else if (!strcasecmp(el, "f_ep_expire_value"))          flbuf.f_ep.expire_value = atoi(ChrPtr(migr_chardata));
622         else return 0;
623         return 1;
624 }
625
626
627 int migr_visitrecord(void *data, const char *el) {
628         if (!strcasecmp(el, "v_roomnum"))                       vbuf.v_roomnum = atol(ChrPtr(migr_chardata));
629         else if (!strcasecmp(el, "v_roomgen"))                  vbuf.v_roomgen = atol(ChrPtr(migr_chardata));
630         else if (!strcasecmp(el, "v_usernum"))                  vbuf.v_usernum = atol(ChrPtr(migr_chardata));
631
632         else if (!strcasecmp(el, "v_seen")) {
633                 int is_textual_seen = 0;
634                 int i;
635                 int max = StrLength(migr_chardata);
636
637                 vbuf.v_lastseen = atol(ChrPtr(migr_chardata));
638                 is_textual_seen = 0;
639                 for (i=0; i < max; ++i) 
640                         if (!isdigit(ChrPtr(migr_chardata)[i]))
641                                 is_textual_seen = 1;
642                 if (is_textual_seen)
643                         safestrncpy(vbuf.v_seen, ChrPtr(migr_chardata), sizeof vbuf.v_seen);
644         }
645
646         else if (!strcasecmp(el, "v_answered"))                 safestrncpy(vbuf.v_answered, ChrPtr(migr_chardata), sizeof vbuf.v_answered);
647         else if (!strcasecmp(el, "v_flags"))                    vbuf.v_flags = atoi(ChrPtr(migr_chardata));
648         else if (!strcasecmp(el, "v_view"))                     vbuf.v_view = atoi(ChrPtr(migr_chardata));
649         else return 0;
650         return 1;
651 }
652
653
654 void migr_xml_end(void *data, const char *el) {
655         const char *ptr;
656         int msgcount = 0;
657         long msgnum = 0L;
658         long *msglist = NULL;
659         int msglist_alloc = 0;
660         // *** GENERAL STUFF ***
661
662         if (!strcasecmp(el, "citadel_migrate_data")) {
663                 --citadel_migrate_data;
664                 return;
665         }
666
667         if (citadel_migrate_data != 1) {
668                 syslog(LOG_ERR, "migrate: out-of-sequence tag <%s> detected.  Warning: ODD-DATA!", el);
669                 return;
670         }
671
672         // syslog(LOG_DEBUG, "END TAG: <%s> DATA: <%s>\n", el, (migr_chardata_len ? migr_chardata : ""));
673
674         // *** CONFIG ***
675
676         if (!strcasecmp(el, "config")) {
677                 syslog(LOG_DEBUG, "migrate: imported config key=%s", ikey);
678
679                 if (ikey != NULL) {
680                         CtdlSetConfigStr(ikey, (char *)ChrPtr(migr_chardata));
681                         free(ikey);
682                         ikey = NULL;
683                 }
684                 else {
685                         syslog(LOG_INFO, "migrate: closed a <config> tag but no key name was supplied.");
686                 }
687         }
688
689         // *** USER ***
690         else if ((!strncasecmp(el, HKEY("u_"))) && 
691                  migr_userrecord(data, el))
692                 ; /* Nothing to do anymore */
693         else if (!strcasecmp(el, "user")) {
694                 CtdlPutUser(&usbuf);
695                 syslog(LOG_INFO, "migrate: imported user: %s", usbuf.fullname);
696         }
697
698         // *** OPENID ***
699
700         else if (!strcasecmp(el, "oid_url"))                    safestrncpy(openid_url, ChrPtr(migr_chardata), sizeof openid_url);
701         else if (!strcasecmp(el, "oid_usernum"))                openid_usernum = atol(ChrPtr(migr_chardata));
702
703         else if (!strcasecmp(el, "openid")) {                   // see serv_openid_rp.c for a description of the record format
704                 char *oid_data;
705                 int oid_data_len;
706                 oid_data_len = sizeof(long) + strlen(openid_url) + 1;
707                 oid_data = malloc(oid_data_len);
708                 memcpy(oid_data, &openid_usernum, sizeof(long));
709                 memcpy(&oid_data[sizeof(long)], openid_url, strlen(openid_url) + 1);
710                 cdb_store(CDB_EXTAUTH, openid_url, strlen(openid_url), oid_data, oid_data_len);
711                 free(oid_data);
712                 syslog(LOG_INFO, "migrate: imported OpenID: %s (%ld)", openid_url, openid_usernum);
713         }
714
715         // *** ROOM ***
716         else if ((!strncasecmp(el, HKEY("QR"))) && 
717                  migr_roomrecord(data, el))
718                 ; // Nothing to do anymore
719         else if (!strcasecmp(el, "room")) {
720                 CtdlPutRoom(&qrbuf);
721                 syslog(LOG_INFO, "migrate: imported room: %s", qrbuf.QRname);
722         }
723
724         // *** ROOM MESSAGE POINTERS ***
725
726         else if (!strcasecmp(el, "FRname")) {
727                 safestrncpy(FRname, ChrPtr(migr_chardata), sizeof FRname);
728         }
729
730         else if (!strcasecmp(el, "FRmsglist")) {
731                 if (!IsEmptyStr(FRname)) {
732                         msgcount = 0;
733                         msglist_alloc = 1000;
734                         msglist = malloc(sizeof(long) * msglist_alloc);
735
736                         syslog(LOG_DEBUG, "migrate: message list for: %s", FRname);
737
738                         ptr = ChrPtr(migr_chardata);
739                         while (*ptr != 0) {
740                                 while ((*ptr != 0) && (!isdigit(*ptr))) {
741                                         ++ptr;
742                                 }
743                                 if ((*ptr != 0) && (isdigit(*ptr))) {
744                                         msgnum = atol(ptr);
745                                         if (msgnum > 0L) {
746                                                 if (msgcount >= msglist_alloc) {
747                                                         msglist_alloc *= 2;
748                                                         msglist = realloc(msglist, sizeof(long) * msglist_alloc);
749                                                 }
750                                                 msglist[msgcount++] = msgnum;
751                                                 }
752                                         }
753                                         while ((*ptr != 0) && (isdigit(*ptr))) {
754                                                 ++ptr;
755                                         }
756                                 }
757                         }
758                         if (msgcount > 0) {
759                                 CtdlSaveMsgPointersInRoom(FRname, msglist, msgcount, 0, NULL, 1);
760                         }
761                         free(msglist);
762                         msglist = NULL;
763                         msglist_alloc = 0;
764                         syslog(LOG_DEBUG, "migrate: imported %d messages.", msgcount);
765                         if (server_shutting_down) {
766                                 return;
767                 }
768         }
769
770         // *** FLOORS ***
771         else if ((!strncasecmp(el, HKEY("f_"))) && 
772                  migr_floorrecord(data, el))
773                 ; // Nothing to do anymore
774
775         else if (!strcasecmp(el, "floor")) {
776                 CtdlPutFloor(&flbuf, floornum);
777                 syslog(LOG_INFO, "migrate: imported floor #%d (%s)", floornum, flbuf.f_name);
778         }
779
780         // *** VISITS ***
781         else if ((!strncasecmp(el, HKEY("v_"))) && migr_visitrecord(data, el)) {
782                 ; // Nothing to do anymore
783         }
784         else if (!strcasecmp(el, "visit")) {
785                 put_visit(&vbuf);
786                 syslog(LOG_INFO, "migrate: imported visit: %ld/%ld/%ld", vbuf.v_roomnum, vbuf.v_roomgen, vbuf.v_usernum);
787         }
788
789         // *** MESSAGES ***
790         
791         else if (!strcasecmp(el, "msg_msgnum"))                 smi.meta_msgnum = import_msgnum = atol(ChrPtr(migr_chardata));
792         else if (!strcasecmp(el, "msg_meta_refcount"))          smi.meta_refcount = atoi(ChrPtr(migr_chardata));
793         else if (!strcasecmp(el, "msg_meta_rfc822_length"))     smi.meta_rfc822_length = atoi(ChrPtr(migr_chardata));
794         else if (!strcasecmp(el, "msg_meta_content_type"))      safestrncpy(smi.meta_content_type, ChrPtr(migr_chardata), sizeof smi.meta_content_type);
795
796         else if (!strcasecmp(el, "msg_text")) {
797                 long rc;
798                 struct CtdlMessage *msg;
799
800                 FlushStrBuf(migr_MsgData);
801                 StrBufDecodeBase64To(migr_chardata, migr_MsgData);
802
803                 msg = CtdlDeserializeMessage(import_msgnum, -1,
804                                              ChrPtr(migr_MsgData), 
805                                              StrLength(migr_MsgData));
806                 if (msg != NULL) {
807                         rc = CtdlSaveThisMessage(msg, import_msgnum, 0);
808                         if (rc == 0) {
809                                 PutMetaData(&smi);
810                         }
811                         CM_Free(msg);
812                 }
813                 else {
814                         rc = -1;
815                 }
816
817                 syslog(LOG_INFO,
818                        "migrate: %s message #%ld, size=%d, ref=%d, body=%ld, content-type: %s",
819                        (rc!= 0)?"failed to import":"imported",
820                        import_msgnum,
821                        StrLength(migr_MsgData),
822                        smi.meta_refcount,
823                        smi.meta_rfc822_length,
824                        smi.meta_content_type
825                 );
826                 memset(&smi, 0, sizeof(smi));
827         }
828
829         // *** MORE GENERAL STUFF ***
830
831         FlushStrBuf(migr_chardata);
832 }
833
834
835 // Import begins here
836 void migr_do_import(void) {
837         XML_Parser xp;
838         char buf[SIZ];
839         
840         unbuffer_output();
841         migr_chardata = NewStrBufPlain(NULL, SIZ * 20);
842         migr_MsgData = NewStrBufPlain(NULL, SIZ * 20);
843         xp = XML_ParserCreate(NULL);
844         if (!xp) {
845                 cprintf("%d Failed to create XML parser instance\n", ERROR+INTERNAL_ERROR);
846                 return;
847         }
848         XML_SetElementHandler(xp, migr_xml_start, migr_xml_end);
849         XML_SetCharacterDataHandler(xp, migr_xml_chardata);
850
851         CC->dont_term = 1;
852
853         cprintf("%d sock it to me\n", SEND_LISTING);
854         unbuffer_output();
855
856         client_set_inbound_buf(SIZ * 10);
857
858         while (client_getln(buf, sizeof buf) >= 0 && strcmp(buf, "000")) {
859                 XML_Parse(xp, buf, strlen(buf), 0);
860                 if (server_shutting_down)
861                         break;  // Should we break or return?
862         }
863
864         XML_Parse(xp, "", 0, 1);
865         XML_ParserFree(xp);
866         FreeStrBuf(&migr_chardata);
867         FreeStrBuf(&migr_MsgData);
868         rebuild_euid_index();
869         rebuild_usersbynumber();
870         CtdlSetConfigInt("MM_fulltext_wordbreaker", -1);        // Set an invalid wordbreaker to force re-indexing
871         CC->dont_term = 0;
872 }
873
874
875 // ******************************************************************************
876 // *                         Dispatcher, Common code                            *
877 // ******************************************************************************
878
879 // Dump out the pathnames of directories which can be copied "as is"
880 void migr_do_listdirs(void) {
881         cprintf("%d Don't forget these:\n", LISTING_FOLLOWS);
882         cprintf("files|%s\n",           ctdl_file_dir);
883         cprintf("messages|%s\n",        ctdl_message_dir);
884         cprintf("keys|%s\n",            ctdl_key_dir);
885         cprintf("000\n");
886 }
887
888
889 // ******************************************************************************
890 // *                    Repair database integrity                               *
891 // ******************************************************************************
892
893 StrBuf *PlainMessageBuf = NULL;
894 HashList *UsedMessageIDS = NULL;
895
896 int migr_restore_message_metadata(long msgnum, int refcount) {
897         struct MetaData smi;
898         struct CtdlMessage *msg;
899         char *mptr = NULL;
900
901         // We can use a static buffer here because there will never be more than
902         // one of this operation happening at any given time, and it's really best
903         // to just keep it allocated once instead of torturing malloc/free.
904         // Call this function with msgnum "-1" to free the buffer when finished.
905         static int encoded_alloc = 0;
906         static char *encoded_msg = NULL;
907
908         if (msgnum < 0) {
909                 if ((encoded_alloc == 0) && (encoded_msg != NULL)) {
910                         free(encoded_msg);
911                         encoded_alloc = 0;
912                         encoded_msg = NULL;
913                         // todo FreeStrBuf(&PlainMessageBuf); PlainMessageBuf = NULL;
914                 }
915                 return 0;
916         }
917
918         if (PlainMessageBuf == NULL) {
919                 PlainMessageBuf = NewStrBufPlain(NULL, 10*SIZ);
920         }
921
922         // Ok, here we go ...
923
924         msg = CtdlFetchMessage(msgnum, 1);
925         if (msg == NULL) {
926                 return 1;
927         }
928
929         GetMetaData(&smi, msgnum);
930         smi.meta_msgnum = msgnum;
931         smi.meta_refcount = refcount;
932         
933         // restore the content type from the message body:
934         mptr = bmstrcasestr(msg->cm_fields[eMesageText], "Content-type:");
935         if (mptr != NULL) {
936                 char *aptr;
937                 safestrncpy(smi.meta_content_type, &mptr[13], sizeof smi.meta_content_type);
938                 striplt(smi.meta_content_type);
939                 aptr = smi.meta_content_type;
940                 while (!IsEmptyStr(aptr)) {
941                         if ((*aptr == ';')
942                             || (*aptr == ' ')
943                             || (*aptr == 13)
944                             || (*aptr == 10)) {
945                                 memset(aptr, 0, sizeof(smi.meta_content_type) - (aptr - smi.meta_content_type));
946                         }
947                         else aptr++;
948                 }
949         }
950
951         CC->redirect_buffer = PlainMessageBuf;
952         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ALL, 0, 1, QP_EADDR);
953         smi.meta_rfc822_length = StrLength(CC->redirect_buffer);
954         CC->redirect_buffer = NULL;
955
956         syslog(LOG_INFO,
957                "migrate: setting message #%ld meta data to: refcount=%d, bodylength=%ld, content-type: %s",
958                smi.meta_msgnum,
959                smi.meta_refcount,
960                smi.meta_rfc822_length,
961                smi.meta_content_type
962         );
963
964         PutMetaData(&smi);
965         CM_Free(msg);
966         return 0;
967 }
968
969
970 void migr_check_room_msg(long msgnum, void *userdata) {
971         fprintf(migr_global_message_list, "%ld %s\n", msgnum, CC->room.QRname);
972 }
973
974
975 void migr_check_rooms_backend(struct ctdlroom *buf, void *data) {
976         // message list goes inside this tag
977         CtdlGetRoom(&CC->room, buf->QRname);
978         CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, migr_check_room_msg, NULL);
979 }
980
981
982 void RemoveMessagesFromRooms(StrBuf *RoomNameVec, long msgnum) {
983         struct MetaData smi;
984         const char *Pos = NULL;
985         StrBuf *oneRoom = NewStrBuf();
986
987         syslog(LOG_INFO, "migrate: removing message pointer %ld from these rooms: %s", msgnum, ChrPtr(RoomNameVec));
988
989         while (Pos != StrBufNOTNULL){
990                 StrBufExtract_NextToken(oneRoom, RoomNameVec, &Pos, '|');
991                 CtdlDeleteMessages(ChrPtr(oneRoom), &msgnum, 1, "");
992         };
993         GetMetaData(&smi, msgnum);
994         AdjRefCount(msgnum, -smi.meta_refcount);
995 }
996
997
998 void migr_do_restore_meta(void) {
999         char buf[SIZ];
1000         int failGetMessage;
1001         long msgnum = 0;
1002         int lastnum = 0;
1003         int refcount = 0;
1004         CitContext *Ctx;
1005         char *prn;
1006         StrBuf *RoomNames;
1007         char cmd[SIZ];
1008
1009         migr_global_message_list = fopen(migr_tempfilename1, "w");
1010         if (migr_global_message_list != NULL) {
1011                 CtdlForEachRoom(migr_check_rooms_backend, NULL);
1012                 fclose(migr_global_message_list);
1013         }
1014
1015         // Process the 'global' message list.  (Sort it and remove dups.
1016         // Dups are ok because a message may be in more than one room, but
1017         // this will be handled by exporting the reference count, not by
1018         // exporting the message multiple times.)
1019         snprintf(cmd, sizeof cmd, "sort -n <%s >%s", migr_tempfilename1, migr_tempfilename2);
1020         if (system(cmd) != 0) {
1021                 syslog(LOG_ERR, "migrate: error %d", errno);
1022         }
1023
1024         RoomNames = NewStrBuf();
1025         Ctx = CC;
1026         migr_global_message_list = fopen(migr_tempfilename2, "r");
1027         if (migr_global_message_list != NULL) {
1028                 syslog(LOG_INFO, "migrate: opened %s", migr_tempfilename1);
1029                 while ((Ctx->kill_me == 0) && 
1030                        (fgets(buf, sizeof(buf), migr_global_message_list) != NULL)) {
1031                         msgnum = atol(buf);
1032                         if (msgnum == 0L) 
1033                                 continue;
1034                         if (lastnum == 0) {
1035                                 lastnum = msgnum;
1036                         }
1037                         prn = strchr(buf, ' ');
1038                         if (lastnum != msgnum) {
1039                                 failGetMessage = migr_restore_message_metadata(lastnum, refcount);
1040                                 if (failGetMessage) {
1041                                         RemoveMessagesFromRooms(RoomNames, lastnum);
1042                                 }
1043                                 refcount = 1;
1044                                 lastnum = msgnum;
1045                                 if (prn != NULL) {
1046                                         StrBufPlain(RoomNames, prn + 1, -1);
1047                                 }
1048                                 StrBufTrim(RoomNames);
1049                         }
1050                         else {
1051                                 if (prn != NULL) {
1052                                         if (StrLength(RoomNames) > 0) {
1053                                                 StrBufAppendBufPlain(RoomNames, HKEY("|"), 0);
1054                                         }
1055                                         StrBufAppendBufPlain(RoomNames, prn, -1, 1);
1056                                         StrBufTrim(RoomNames);
1057                                 }
1058                                 refcount ++;
1059                         }
1060                         lastnum = msgnum;
1061                 }
1062                 failGetMessage = migr_restore_message_metadata(msgnum, refcount);
1063                 if (failGetMessage) {
1064                         RemoveMessagesFromRooms(RoomNames, lastnum);
1065                 }
1066                 fclose(migr_global_message_list);
1067         }
1068
1069         migr_restore_message_metadata(-1L, -1); // This frees the encoding buffer
1070         cprintf("%d system analysis completed", CIT_OK);
1071         Ctx->kill_me = 1;
1072 }
1073
1074
1075 // ******************************************************************************
1076 // *                         Dispatcher, Common code                            *
1077 // ******************************************************************************
1078 void cmd_migr(char *cmdbuf) {
1079         char cmd[32];
1080         
1081         if (CtdlAccessCheck(ac_aide)) return;
1082         
1083         if (CtdlTrySingleUser()) {
1084                 CtdlDisableHouseKeeping();
1085                 CtdlMakeTempFileName(migr_tempfilename1, sizeof migr_tempfilename1);
1086                 CtdlMakeTempFileName(migr_tempfilename2, sizeof migr_tempfilename2);
1087
1088                 extract_token(cmd, cmdbuf, 0, '|', sizeof cmd);
1089                 if (!strcasecmp(cmd, "export")) {
1090                         migr_do_export();
1091                 }
1092                 else if (!strcasecmp(cmd, "import")) {
1093                         migr_do_import();
1094                 }
1095                 else if (!strcasecmp(cmd, "listdirs")) {
1096                         migr_do_listdirs();
1097                 }
1098                 else if (!strcasecmp(cmd, "restoremeta")) {
1099                         migr_do_restore_meta();
1100                 }
1101                 else {
1102                         cprintf("%d illegal command\n", ERROR + ILLEGAL_VALUE);
1103                 }
1104
1105                 unlink(migr_tempfilename1);
1106                 unlink(migr_tempfilename2);
1107
1108                 CtdlEnableHouseKeeping();
1109                 CtdlEndSingleUser();
1110         }
1111         else {
1112                 cprintf("%d The migrator is already running.\n", ERROR + RESOURCE_BUSY);
1113         }
1114 }
1115
1116
1117 // ******************************************************************************
1118 // *                              Module Hook                                   *
1119 // ******************************************************************************
1120
1121 CTDL_MODULE_INIT(migrate)
1122 {
1123         if (!threading)
1124         {
1125                 CtdlRegisterProtoHook(cmd_migr, "MIGR", "Across-the-wire migration");
1126         }
1127         
1128         // return our module name for the log
1129         return "migrate";
1130 }