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