1<?php
2
3/* This script is in the public domamin.  Please give it out freely to help the spread of atheme, and assist others in your position. */
4/*
5convert.php is a work in progress, but it has successfully converted production
6databases.  It is to be considered a release candidate.  It reads an ircservices
7XML database export from STDIN and prints a atheme database to STDOUT.
8
9NOTE: The use of a custom password encryption module (included) is required!
10      It (ircs_crypto_trans.c) replaces the current ircservices.c in modules/crypto/.
11
12The password encryption module includes code to allow a transition to the
13encryption module of your choice.  Uncommenting the following line will
14cause the module to execute the listed program every time a password is hashed.
15The program recieves two lines on stdin, newline separated.  They are
16the unencrypted password followed by the currently recorded password.  Your
17program may encrypt the unencrypted password however it chooses and store it
18with the pre-encrypted version.  After all nicks have been identified to
19(after the expiry time for nicks), use this map to replace the passwords stored
20in the database.  The password location in the database is shown below.
21
22NOTE: There is no harm in leaving some few ircservices passwords in the
23      database after switching to POSIX (others not tested), if you so choose.
24      Those nicks will simply not be able to log in, and need their password
25      reset, such as by the use of SENDPASS.
26
27Program Activation Line:
28// #define PW_TRANSITION_LOG "./pwtransition.sh"
29
30Password Location:
31MU nick CRYPTED_PW ...
32
33NOTE: if channels for some reason cannot be converted, errors.log will be populated with
34information about what channels & why.
35
36Bugs & Patches to Jason@WinSE.ath.cx
37*/
38
39$nicks = array();
40$chans = array();
41
42
43$raw_lines = array();
44while (!feof(STDIN))
45	$raw_lines[] = html_entity_decode(fgets(STDIN));
46
47
48/* Read in and parse IRCservices
49 */
50$category = '';
51foreach($raw_lines as $raw_line)
52{
53	$line = trim($raw_line);
54
55	if ($line == '<nickgroupinfo>')
56		$category = 'nickgroupinfo';
57	if ($line == '<nickinfo>')
58		$category = 'nickinfo';
59	if ($line == '<channelinfo>')
60		$category = 'channelinfo';
61	if ($line == '</'. $category .'>')
62		$category = '';
63
64	switch ($category)
65	{
66		case 'nickgroupinfo': read_nickgroupinfo($line); break;
67		case 'nickinfo': read_nickinfo($line); break;
68		case 'channelinfo': read_channelinfo($line); break;
69	}
70}
71
72//print_r($nicks);
73//print_r($chans);
74
75/* Print out atheme
76 */
77$MUout = 0;
78$MCout = 0;
79$CAout = 0;
80
81printf("DBV 6\n");
82printf("CF +vVoOtsriRfhHAb\n");
83
84// Nicks
85foreach ($nicks as $id => $data)
86{
87	$first_reg = 0;
88	$last_login = 0;
89	$nick_vhost = '';
90	$nick_rhost = '';
91	$nick_noexpire = false;
92	foreach ($data['nicks'] as $nick)
93	{
94		$first_reg = ($first_reg == 0 ? $nick['time_registered'] : min($first_reg,$nick['time_registered']));
95		if ($last_login < $nick['last_seen'])
96		{
97			$last_login = $nick['last_seen'];
98			$nick_rhost = $nick['realhost'];
99			$nick_vhost = $nick['fakehost'];
100		}
101		$nick_noexpire = $nick_noexpire || $nick['noexpire'];
102	}
103	$password = substr(bin2hex(de_ircs($data['pass'],false)),0,16);
104	if (!$password)
105		$password = "nopass";
106	printf("MU %s %s %s %u %u 0 0 0 %u\n",
107		$data['mainnick'],
108		$password,						// My god, what were they smoking?!
109									// Atheme's IRCservices compatibility module had to be edited.
110									// IRCservices database dump gives custom-encoded md5()+remainder_of_buffer
111		$data['email'] ? $data['email'] : 'nomail',
112		$first_reg,
113		$last_login,
114		(1280 | ( $nick_noexpire ? 0x1 : 0 ) | ( $data['hide-email'] ? 0x10 : 0 )) & ~0x100
115	      );
116	$MUout++;
117
118	printf("MD U %s private:host:vhost %s\n",$data['mainnick'],$nick_vhost);
119	printf("MD U %s private:host:actual %s\n",$data['mainnick'],$nick_rhost);
120	if ($data['enforce'])
121		printf("MD U %s private:doenforce 1\n",$data['mainnick']);
122
123	foreach ($data['nicks'] as $nick)
124		printf("MN %s %s %u %u\n",
125			$data['mainnick'],
126			$nick['nick'],
127			$nick['time_registered'],
128			$nick['last_seen']
129		      );
130
131	if (isset($data['access_list']))
132		foreach ($data['access_list'] as $entry)
133			printf("AC %s %s\n", $data['mainnick'], $entry);
134
135	if (isset($data['memos']))
136		foreach ($data['memos'] as $memo)
137			printf("ME %s %s %u %u %s\n",
138				$data['mainnick'],
139				$memo['from'],
140				$memo['time'],
141				$memo['read'],
142				$memo['text']
143			      );
144
145	$sAjoin = "";
146	if (isset($data['ajoin']))
147		foreach ($data['ajoin'] as $sChan)
148			$sAjoin .= $sChan . ",";
149
150	if (!empty($sAjoin))
151	{
152		printf("MD U %s private:autojoin %s\n", $data['mainnick'], $sAjoin);
153	}
154}
155
156// Chans
157foreach ($chans as $name => $data)
158{
159	if (empty($name) || empty($nicks[$data['founder']]['mainnick']) ||
160			empty($data['time_registered']) || empty($data['time_used']) ||
161			empty($data['mlock_on']) || empty($data['mlock_off']) ||
162			empty($data['mlock_limit']))
163	{
164		$sErrors[] = "*** WARNING: potentially skipping entry for $name";
165
166		$bFatal = false;
167
168		if (empty($name))
169		{
170			$sErrors[] = "Name is empty."; // should never happen
171			$bFatal = true;
172		}
173		if (empty($nicks[$data['founder']]['mainnick']))
174		{
175			$sErrors[] = "Main nick is empty"; // forbidden channels can get this
176			$bFatal = true;
177		}
178		if (empty($data['time_registered']))
179		{
180			$sErrors[] = "Time registered is empty";
181			$bFatal = true;
182		}
183		if (empty($data['time_used']))
184		{
185			$sErrors[] = "Time used is empty";
186			$bFatal = true;
187		}
188		if (empty($data['mlock_on']))
189		{
190			$sErrors[] = "MLOCK on is empty";
191			$data['mlock_on'] = 0;
192		}
193		if (empty($data['mlock_off']))
194		{
195			$sErrors[] = "MLOCK off is empty";
196			$data['mlock_off'] = 0;
197		}
198		if (empty($data['mlock_limit']))
199		{
200			$sErrors[] = "MLOCK limit is empty";
201			$data['mlock_limit'] = 0;
202		}
203
204		if ($bFatal)
205		{
206			$sErrors[] = "Cannot write this channel, ignoring";
207			continue;
208		}
209	}
210
211	printf("MC %s 0 %s %u %u 64 %u %u %u\n",
212		$name,
213		$nicks[$data['founder']]['mainnick'],
214		$data['time_registered'],
215		$data['time_used'],
216		$data['mlock_on'],
217		$data['mlock_off'],
218		$data['mlock_limit']
219	      );
220	$MCout++;
221
222	if (isset($data['topic']))
223	{
224		printf("MD C %s private:topic:setter %s\n", $name, ($data['topic_by'] == '<unknown>' ? 'IRC' : $data['topic_by']) );
225		printf("MD C %s private:topic:text %s\n", $name, $data['topic']);
226		printf("MD C %s private:topic:ts %u\n", $name, $data['topic_time']);
227	}
228
229	foreach ($data['access'] as $id => $flags)
230	{
231		if ($id{0} == '+')
232			$id = $nicks[substr($id,1)]['mainnick'];
233		else
234			$id = substr($id,1);
235
236		printf("CA %s %s %s %u\n",
237			$name,
238			$id,
239			$flags,
240			0 // Timestamp.  Hopefully, 0 is good enough.  Not sure how exactly atheme's compatibility code works.
241		      );
242		$CAout++;
243	}
244
245	if (isset($data['akicks']))
246		foreach ($data['akicks'] as $akick)
247		{
248			printf("CA %s %s +b %u\n",
249				$name,
250				$akick['mask'],
251				$akick['set']
252			      );
253			$CAout++;
254
255			printf("MD A %s:%s reason %s\n",
256				$name,
257				$akick['mask'],
258				$akick['reason']
259			      );
260		}
261}
262
263printf("DE %d %d %d 0", $MUout, $MCout, $CAout );
264
265
266
267file_put_contents("errors.log", implode("\n", $sErrors));
268
269
270
271
272/* Helper functions
273 */
274function read_nickgroupinfo($line)
275{
276	global $nicks;
277	static $id = 0;
278	static $section = '';
279	static $memo_num = 0;
280
281	if ($line == '<nickgroupinfo>')
282		$id = 0;
283
284	if ($line == '<memo>')
285		$section = 'memo';
286	if (substr($line,0,7) == '<nicks ')
287		$section = 'nicks';
288	if (substr($line,0,8) == '<access ')
289		$section = 'access';
290	if (substr($line, 0, 7) == '<ajoin ')
291		$section = 'ajoin';
292	if ($line == '</'. $section .'>')
293		$section = '';
294
295	if ($section == '' && preg_match('~^<([a-z_]+)(?: [^>]+)?>(.*)</\1>$~', $line, &$parts) > 0)
296// XXX old ircservices.	if ($section == '' && preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
297	{
298		if ($parts[1] == 'id')
299			$id = $parts[2];
300
301		if ($parts[1] == 'pass')
302			$nicks[$id]['pass'] = $parts[2];
303
304		if ($parts[1] == 'email')
305			$nicks[$id]['email'] = $parts[2];
306
307		if ($parts[1] == 'flags')
308		{
309			$nicks[$id]['hide-email'] = $parts[2] & 0x80;
310			$nicks[$id]['enforce'] = $parts[2] & 0x1;
311		}
312	}
313	elseif ($section == 'memo' && preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
314	{
315		if ($parts[1] == 'number')
316			$memo_num = $parts[2];
317
318		if ($parts[1] == 'flags')
319			$nicks[$id]['memos'][$memo_num]['read'] = ( $parts[2] == 0 ? true : false );
320
321		if ($parts[1] == 'sender')
322			$nicks[$id]['memos'][$memo_num]['from'] = $parts[2];
323
324		if ($parts[1] == 'time')
325			$nicks[$id]['memos'][$memo_num]['time'] = $parts[2];
326
327		if ($parts[1] == 'text')
328			$nicks[$id]['memos'][$memo_num]['text'] = de_ircs($parts[2],true);
329	}
330	elseif ($section == 'nicks' && preg_match('~^<([a-z_-]+)>(.*)</\1>$~',$line,&$parts) > 0)
331	{
332		if ($parts[1] == 'array-element' && !isset($nicks[$id]['mainnick']))
333			$nicks[$id]['mainnick'] = $parts[2];
334	}
335	elseif ($section == 'ajoin' && preg_match('~^<([a-z_-]+)>(.*)</\1>$~',$line,&$parts) > 0)
336	{
337		if ($parts[1] == 'array-element')
338			$nicks[$id]['ajoin'][] = $parts[2];
339	}
340	elseif ($section == 'access' && preg_match('~^<([a-z_-]+)>(.*)</\1>$~',$line,&$parts) > 0)
341	{
342		if ($parts[1] == 'array-element')
343			$nicks[$id]['access_list'][] = $parts[2];
344	}
345}
346
347function read_nickinfo($line)
348{
349	global $nicks;
350	static $data = array();
351
352	if ($line == '<nickinfo>')
353		$data = array();
354
355	if (preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
356	{
357		if ($parts[1] == 'nick')
358			$data['nick'] = $parts[2];
359
360		if ($parts[1] == 'last_usermask')
361			$data['fakehost'] = $parts[2];
362
363		if ($parts[1] == 'last_realmask')
364			$data['realhost'] = $parts[2];
365
366		if ($parts[1] == 'time_registered')
367			$data['time_registered'] = $parts[2];
368
369		if ($parts[1] == 'last_seen')
370			$data['last_seen'] = $parts[2];
371
372		if ($parts[1] == 'status')
373			$data['noexpire'] = ( $parts[2] == 4 ? true : false );
374
375		if ($parts[1] == 'nickgroup')
376		{
377			// Seriously...
378			if ($parts[2] == 0)
379				return;
380
381			// Now that we have an id to associate it with, commit.
382			$nickid = count($nicks[$parts[2]][nicks]);
383			foreach($data as $type => $info)
384				$nicks[$parts[2]][nicks][$nickid][$type] = $info;
385		}
386	}
387}
388
389function read_channelinfo($line)
390{
391	global $chans;
392	static $chan = '';
393	static $levels = array();
394	static $akick = array();
395	static $acc_who = 0;
396	static $section = '';
397	static $mode_map = array(
398				'i'=>0x00000001, 'l'=>0x00000004, 'm'=>0x00000008, 'n'=>0x00000010, 'p'=>0x00000040,
399				's'=>0x00000080, 't'=>0x00000100, 'c'=>0x00001000, 'M'=>0x00002000, 'R'=>0x00004000,
400				'O'=>0x00008000, 'A'=>0x00010000, 'Q'=>0x00020000, 'S'=>0x00040000, 'K'=>0x00080000,
401				'V'=>0x00100000, 'C'=>0x00200000, 'u'=>0x00400000, 'z'=>0x00800000, 'N'=>0x01000000,
402				'G'=>0x04000000
403				);
404		// Above mode_map is for UnrealIRCd.
405
406
407	if ($line == '<channelinfo>')
408	{
409		$levels = array('b'=>-100,
410				'A'=>0,
411				'v'=>30, 'V'=>30,
412				'h'=>40, 'H'=>40, 'r'=>40, 't'=>40,
413				'o'=>50, 'O'=>50, 'i'=>50,
414				'f'=>100,'R'=>100,
415				's'=>1000
416				);
417	}
418
419	if ($line == '<levels>')
420		$section = 'levels';
421	if ($line == '<chanaccess>')
422		$section = 'chanaccess';
423	if ($line == '<akick>')
424		$section = 'akick';
425	if ($line == '</'. $section .'>')
426		$section = '';
427
428	if ($section == '' && preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
429	{
430		if ($parts[1] == 'name')
431			$chan = $parts[2];
432
433		if ($parts[1] == 'founder')
434		{
435			$chans[$chan]['founder'] = $parts[2];
436			$chans[$chan]['access']['+'.$parts[2]] = '+voOtsriRfhA';
437		}
438
439		if ($parts[1] == 'time_registered')
440			$chans[$chan]['time_registered'] = $parts[2];
441
442		if ($parts[1] == 'last_used')
443			$chans[$chan]['time_used'] = $parts[2];
444
445		if ($parts[1] == 'last_topic')
446			$chans[$chan]['topic'] =  de_ircs($parts[2],true); // Some control codes can segfault atheme
447
448		if ($parts[1] == 'last_topic_setter')
449			$chans[$chan]['topic_by'] = de_ircs($parts[2],true);
450
451		if ($parts[1] == 'last_topic_time')
452			$chans[$chan]['topic_time'] = $parts[2];
453
454		if ($parts[1] == 'mlock_on')
455		{
456			$chans[$chan]['mlock_on'] = 0;
457			for ($i = 0; $i < strlen($parts[2]); $i++)
458			{
459				if ($parts[2]{$i} == ' ')
460					break;
461				if (array_key_exists($parts[2]{$i}, $mode_map))
462					$chans[$chan]['mlock_on'] = $chans[$chan]['mlock_on'] | $mode_map[$parts[2]{$i}];
463			}
464		}
465
466		if ($parts[1] == 'mlock_off')
467		{
468			$chans[$chan]['mlock_off'] = 0;
469			for ($i = 0; $i < strlen($parts[2]); $i++)
470			{
471				if ($parts[2]{$i} == ' ')
472					break;
473				if (array_key_exists($parts[2]{$i}, $mode_map))
474					$chans[$chan]['mlock_off'] = $chans[$chan]['mlock_off'] | $mode_map[$parts[2]{$i}];
475			}
476		}
477
478		if ($parts[1] == 'mlock_limit')
479			$chans[$chan]['mlock_limit'] = $parts[2];
480	}
481	elseif ($section == 'levels' && preg_match('~.*<([a-zA-Z_]+)>(.+)</\1>.*~',$line,&$parts) > 0)
482	{
483		if ($parts[1] == 'CA_INVITE')		$levels['i'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
484		if ($parts[1] == 'CA_SET')		$levels['s'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
485		if ($parts[1] == 'CA_AUTOOP')		$levels['O'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
486		if ($parts[1] == 'CA_AUTOHALFOP')	$levels['H'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
487		if ($parts[1] == 'CA_AUTOVOICE')	$levels['V'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
488		if ($parts[1] == 'CA_AUTOPROTECT')		$levels['a'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
489		if ($parts[1] == 'CA_OPDEOP')		$levels['o'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
490		if ($parts[1] == 'CA_HALFOP')	{	$levels['h'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
491							$levels['t'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
492						}
493		if ($parts[1] == 'CA_VOICE')		$levels['v'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
494		if ($parts[1] == 'CA_ACCESS_LIST')	$levels['A'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
495		if ($parts[1] == 'CA_ACCESS_CHANGE')	$levels['f'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
496		if ($parts[1] == 'CA_CLEAR')		$levels['R'] = ($parts[2] == -1000 ? 1000 : $parts[2]);
497		if ($parts[1] == 'CA_NOJOIN')		$levels['b'] = $parts[2];
498	}
499	elseif ($line == '</levels>')
500	{
501		$acc_anon = '+';
502		foreach($levels as $flag => $level)
503		{
504			if ($flag == 'b' && 0 <= $level)
505				$acc_anon .= $flag;
506			elseif ($flag != 'b' && 0 >= $level)
507				$acc_anon .= $flag;
508		}
509		if ($acc_anon != '+')
510			$chans[$chan]['access']['-*!*@*'] = $acc_anon;
511	}
512	elseif ($section == 'chanaccess' && preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
513	{
514		if ($parts[1] == 'nickgroup')
515			$acc_who = '+' . $parts[2];
516		if ($parts[1] == 'level' && $acc_who != 0)
517		{
518			$chans[$chan]['access'][$acc_who] = '+';
519			foreach($levels as $flag => $level)
520			{
521				if ($flag == 'b' && $parts[2] <= $level)
522					$chans[$chan]['access'][$acc_who] .= $flag;
523				elseif ($flag != 'b' && $parts[2] >= $level)
524					$chans[$chan]['access'][$acc_who] .= $flag;
525			}
526		}
527	}
528	elseif ($section == 'akick' && preg_match('~^<([a-z_]+)>(.*)</\1>$~',$line,&$parts) > 0)
529	{
530		if ($parts[1] == 'mask')
531			$akick['mask'] = $parts[2];
532		if ($parts[1] == 'reason')
533			$akick['reason'] = $parts[2];
534		if ($parts[1] == 'who')
535			$akick['by'] = $parts[2];
536		if ($parts[1] == 'set')
537		{
538			$akick['set'] = $parts[2];
539			if (isset($akick['mask'])) // Sigh.  Empty akicks!  Woo!
540			$chans[$chan]['akicks'][] = $akick;
541			$akick = array();
542		}
543	}
544}
545
546function de_ircs($in,$safe)
547{
548	$in_escape = false;
549	$buf = '';
550	$out = '';
551	for ($i = 0; $i < strlen($in); $i++)
552	{
553		if ($in{$i} == '&')
554		{
555			$buf = '';
556			$in_escape = true;
557		}
558		elseif ($in_escape)
559		{
560			if ($in{$i} == ';')
561			{
562				$in_escape = false;
563				switch ($buf{0})
564				{
565					case 'l': $out .= '<'; break;
566					case 'g': $out .= '>'; break;
567					case 'a': $out .= '&'; break;
568					case '#':
569						if ($safe)
570							switch (substr($buf,1))
571							{
572								case 3:		// color
573								case 2:		// bold
574								case 31:	// underline
575								case 22:	// reverse
576									$out .= chr(substr($buf,1));
577							}
578						else
579							$out .= chr(substr($buf,1));
580						break;
581				}
582				$in_escape = false;
583			}
584			else
585				$buf .= $in{$i};
586		}
587		else
588			$out .= $in{$i};
589	}
590	return $out;
591}
592
593?>
594