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