<?php
/**
 * phpBB to CMSity forum database converter.
 *
 * This script can convert a phpBB 2.0.X (www.phpbb.com) forum 
 * database to CMSity forum (current node).
 *
 * Based on phpBB to BBPress converter by Jaime GÓMEZ OBREGÓN
 * and the previous work of Bruno Torres and The phpBB Group.
 *
 * Copyright (c)  Adam Strzelecki <http://www.nanoant.com/>
 *                ITEISA <http://www.iteisa.com/>
 *                Jaime GÓMEZ OBREGÓN (jaime@iteisa.com)
 * 
 * Parts Copyright (c) Bruno Torres <http://www.brunotorres.net/> and
 * The phpBB Group <http://www.phpbb.com/>
 *
 * Licensed under The GPL License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright     Copyright (c) 2006, Adam Strzelecki, ITEISA, Bruno Torres and The phpBB Group.
 * @link          http://www.iteisa.com/phpbb2bbpress/
 * @license       http://www.gnu.org/copyleft/gpl.html
 *
 * (1) Add following lines to your config.php and
 *     set the accordingly to your phpBB configuration:
 *
 * define('DB_PHPBB_HOSTNAME',    'localhost');
 * define('DB_PHPBB_USERNAME',    'root');
 * define('DB_PHPBB_PASSWORD',    '');
 * define('DB_PHPBB_DATABASE',    'phpbb');
 * define('DB_PHPBB_TABLEPREFIX', 'phpbb_');
 * define('PHPBB_PATH_PREFIX',    'phpbb/');
 * define('PHPBB_AVATAR_SALT',    '12345678901234567890123456789901');
 *
 * (2) Log in to CMSity administrator and create a "forum" node.
 * (3) Launch http://www.mysite.com/forum?import-phpbb to import phpBB database.
 *
 * It will import all phpBB users, forums and posts into specified node.
 * You may re-run import, so it will re-import all forum data again.
 *
 * Remember to backup your databases prior running the import.
 */

if(!t_is('administrator')) a_render(403);

set_time_limit(0);
$bbcode_tpl = null;

$phpbb_forums      = DB_PHPBB_TABLEPREFIX . 'forums';
$phpbb_users       = DB_PHPBB_TABLEPREFIX . 'users';
$phpbb_topics      = DB_PHPBB_TABLEPREFIX . 'topics';
$phpbb_posts       = DB_PHPBB_TABLEPREFIX . 'posts';
$phpbb_posts_text  = DB_PHPBB_TABLEPREFIX . 'posts_text';
$phpbb_attachments = DB_PHPBB_TABLEPREFIX . 'attachments';

header('Content-type: text/plain');

global $cms_user, $cms_node;

echo "Connecting to the phpbb...\n";

// Connecting to the old forum database
$link = @mysql_connect(DB_PHPBB_HOSTNAME, DB_PHPBB_USERNAME, DB_PHPBB_PASSWORD, true) or die("Unable to connect to the phpBB database".mysql_error());

echo "Connected to the phpBB database host.\n"; flush();

@mysql_select_db(DB_PHPBB_DATABASE, $link) or die("Unable to select the phpBB database".mysql_error());
mysql_query("SET CHARACTER SET utf8", $link);

echo "Selected the phpBB database.\n"; flush();

echo "[1] Forums\n"; flush();
$sql = "SELECT parent_id, forum_id, forum_name, forum_desc, forum_topics, forum_posts FROM $phpbb_forums ORDER BY parent_id, forum_id";
$res = mysql_query($sql, $link) or die("Unable to retrieve the data from the phpBB forums table\n$sql\n".mysql_error());
$forums = array();

// Prune old forums
while(l_each('forum')) { a_delete(); }
while($row = mysql_fetch_object($res)) {
	echo '+'; flush();
	$forums[$row->forum_id] = cms_db_insert('node', array(
		'id'       => $row->parent_id ? $forums[$row->parent_id] : $cms_node['id'],
		'type'     => 'forum',
		'name'     => cms_sanitize($row->forum_name),
		'title'    => $row->forum_name,
		'content'  => $row->forum_desc,
		'position' => $row->forum_id,
		'owner'    => $cms_user['id'],
		'editor'   => $cms_user['id']
	), O_DB_AUTO_NAME);
}
echo "\n"; flush();

echo "[2] Users\n"; flush();
$sql = "SELECT user_id, user_avatar, username, user_password, user_email, user_website, user_occ, user_interests, user_regdate, user_lastvisit FROM $phpbb_users";
$res = mysql_query($sql, $link) or die("Unable to retrieve the data from the phpBB users table\n$sql\n".mysql_error());
clear_slugs();
$users = array();
if(chdir(dirname(__FILE__) . '/avatars')) {
while($row = mysql_fetch_object($res)) {
	if(!$row->user_password) continue;
	$regdate = date("Y-m-d H:i:s", $row->user_regdate);
	$last = $row->user_lastvisit ? date("Y-m-d H:i:s", $row->user_lastvisit) : null;
	$users[$row->user_id] = cms_db_insert('user', array(
		'login'    => ($login = make_slug($row->username)),
		'display'  => $row->username,
		'email'    => $row->user_email,
		'website'  => $row->user_website,
		'about'    => $row->user_occ,
		'password' => $row->user_password,
		'created'  => $regdate,
		'updated'  => $regdate,
		'last'  => $last
	));
	if(!$users[$row->user_id]) {
		echo '*'; flush();
		$user = false;
		if(($user = cms_db_select('user', array('id'), array('email' => $row->user_email))) && count($user)) {
			$user = $user[0];
		} elseif(($user = cms_db_select('user', array('id'), array('login' => $login))) && count($user)) {
			$user = $user[0];
		} else {
			echo("WARNING: Login $login or email {$row->user_email} already exists.\n"); flush();
		}
		if($user) {
			$users[$row->user_id] = $user['id'];
		}
	} else {
		echo '+'; flush();
	}
	/* Avatars processing */
	if($row->user_avatar) {
		$info = pathinfo($row->user_avatar);
		$ext = $info['extension'] ? $info['extension'] : 'dat';
		$avatars = array($row->user_avatar, PHPBB_AVATAR_SALT . '_' . $row->user_id . ".$ext");
		foreach($avatars as $avatar) if(file_exists(PHPBB_PATH_PREFIX . $avatar)) {
			$name = $users[$row->user_id] . '.' . $ext;
			$dest = PHPBB_PATH_PREFIX . $avatar;
			if(file_exists($name)) @unlink($name);
			echo '#'; flush();
			if(!@symlink($dest, $name)) {
				echo "\n  SYMLINK FAILED: $name -> $dest\n";
				if(!@copy($dest, $name)) {
					echo "  COPY FAILED: $name <- $dest\n";
				}
			}
		}
	}
} } else {
	echo "  CHDIR FAILED: ".dirname(__FILE__) . '/avatars'."\n";
}
echo "\n"; flush();

echo "[3] Topics\n"; flush();
$sql = "SELECT * FROM $phpbb_topics";
$res = mysql_query($sql, $link) or die("Unable to retrieve the data from the phpBB topics table\n$sql\n".mysql_error());
$topics_f = $topics = array();
while($row = mysql_fetch_object($res)) {
	echo '+'; flush();
	$topic_sticky = 0;
	$topic_open = 0;
	if($row->topic_status == 1) {
		if($row->topic_type == 1) {
			$topic_open = 1;
			$topic_sticky = 3;
		} else {
			$topic_open = 0;
			$topic_sticky = 0;
		}
	} else {
		if($row->topic_type == 2) {
			$topic_open = 1; 
			$topic_sticky = 2;
		} elseif($row->topic_type == 1) {
			$topic_open = 1; 
			$topic_sticky = 1;
		} else {
			$topic_open = 1;
			$topic_sticky = 0;
		}
	}

	if(!array_key_exists($row->forum_id, $forums))
		die("Forum {$row->forum_id} not found.");
	$topics_f[$row->topic_id] = 0;
	$topics[$row->topic_id] = cms_db_insert('node', array(
		'id'       => $forums[$row->forum_id],
		'type'     => 'topic',
		'title'    => html_entity_decode($row->topic_title),
		'name'     => cms_sanitize(html_entity_decode($row->topic_title)),
		'owner'    => $row->topic_poster && array_key_exists($row->topic_poster, $users) ? $users[$row->topic_poster] : null,
		'position' => (0 - $topic_sticky),
		'locked'   => $topic_open ? 0 : 1
	), O_DB_AUTO_NAME);
}
echo "\n"; flush();

echo "[4] Posts\n"; flush();
$sql = "SELECT post_id, forum_id, topic_id, poster_id, post_edit_time, post_edit_user, post_time, poster_ip, bbcode_uid, post_text, post_username FROM $phpbb_posts ORDER BY topic_id, post_id";
$res = mysql_query($sql, $link) or die("Unable to retrieve the data from the phpBB posts and posts_text tables\n$sql\n".mysql_error());
$posts = array();
$posts_o = array();
$posts_a = array();
$posts_c = array();
while($row = mysql_fetch_object($res)) {
	echo '+'; flush();
	$post_time = date("Y-m-d H:i:s", $row->post_time);
	$edit_time = $row->post_edit_time ? date("Y-m-d H:i:s", $row->post_edit_time) : $post_time;
	if(!array_key_exists($row->topic_id, $topics_f)) die("Invalid post {$row->topic_id}.");
	$text = bbencode_second_pass(smilies_pass(nl2p($row->post_text)), $row->bbcode_uid);
	if(strpos($text, 'postlink')) {
		echo "\nPOSTLINK: $row->post_text\n";
	}
	if(!$topics_f[$row->topic_id]) {
		$topics_f[$row->topic_id] = 1;
		cms_db_update('node', array(
			'content' => $text,
			'owner'   => $row->poster_id && array_key_exists($row->poster_id, $users) ? $users[$row->poster_id] : null,
			'author'  => !$row->poster_id || !array_key_exists($row->poster_id, $users) ? $row->post_username : null,
			'created' => $post_time,
			'updated' => $edit_time
			), array('id' => $topics[$row->topic_id]), true);
		$posts[$row->post_id] = $topics[$row->topic_id];
	} else {
		$posts[$row->post_id] = cms_db_insert('node', array(
			'id'      => $topics[$row->topic_id],
			'type'    => 'reply',
			'content' => $text,
			'owner'   => $row->poster_id && array_key_exists($row->poster_id, $users) ? $users[$row->poster_id] : null,
			'author'  => !$row->poster_id || !array_key_exists($row->poster_id, $users) ? $row->post_username : null,
			'editor'  => $row->poster_edit_user && array_key_exists($row->poster_edit_user, $users) ? $users[$row->poster_edit_user] :
			             $row->poster_id && array_key_exists($row->poster_id, $users) ? $users[$row->poster_id] : null,
			'created' => $post_time,
			'updated' => $edit_time
		));
	}
	if($row->poster_id && array_key_exists($row->poster_id, $users)) $posts_o[$row->post_id] = $users[$row->poster_id];
	if(!$row->poster_id || !array_key_exists($row->poster_id, $users)) $posts_a[$row->post_id] = $row->post_username;
	$posts_c[$row->post_id] = $edit_time;
}
echo "\n"; flush();

echo "[5] Attachments\n"; flush();
$sql = "SELECT physical_filename, real_filename, download_count, extension, mimetype, post_msg_id FROM $phpbb_attachments";
$res = mysql_query($sql, $link) or die("Unable to retrieve the data from the phpBB attachements tables\n$sql\n".mysql_error());
if(chdir(dirname(__FILE__) . '/upload')) {
while($row = mysql_fetch_object($res)) {
	echo '+'; flush();
	if(!$row->post_msg_id) continue;
	if(!array_key_exists($row->post_msg_id, $posts)) die("Unknown post {$row->post_msg_id} for attachment.");
	$id = cms_db_insert('node', array(
			'id'       => $posts[$row->post_msg_id],
			'type'     => 'file',
			'owner'    => array_key_exists($row->post_msg_id, $posts_o) ? $posts_o[$row->post_msg_id] : null,
			'author'   => array_key_exists($row->post_msg_id, $posts_a) ? $posts_a[$row->post_msg_id] : null,
			'content'  => $row->mimetype,
			'title'    => $row->real_filename,
			'hits'     => $row->download_count,
			'name'     => cms_sanitize($row->real_filename, false),
			'created'  => $posts_c[$row->post_msg_id],
			'updated'  => $posts_c[$row->post_msg_id]
		), O_DB_AUTO_NAME);
	if(!$id) continue;
	$name = join(',', $id) . "." . $row->extension;
	$dest = PHPBB_PATH_PREFIX . $row->physical_filename;
	if(!@symlink($dest, $name)) {
		echo "\n  SYMLINK FAILED: $name -> $dest\n";
		if(!@copy($dest, $name)) {
			echo "  COPY FAILED: $name <- $dest\n";
		}
	}
} } else {
	echo "  CHDIR FAILED: ".dirname(__FILE__) . '/upload'."\n";
}
echo "\n"; flush();

echo "[6] Counting last dates.\n"; flush();

$sid = cms_db_serialize_id($cms_node['id'], '%');
cms_db_query(<<<QUERY
UPDATE node, (SELECT p.id AS lastid, MAX(c.updated) AS modified FROM node AS p LEFT JOIN node AS c ON INSTR(c.id, p.id)=1 WHERE p.id LIKE $sid GROUP BY p.id) AS l SET last = modified WHERE id = lastid
QUERY
);

echo "[7] Optimizing tables.\n"; flush();

cms_db_query('OPTIMIZE TABLE user');
cms_db_query('OPTIMIZE TABLE node');

echo '[X] Done.';

function sanitize_attachments($a) {
	return cms_sanitize($a[1], false);
}

function nl2p($text) {
	$text = str_replace("\n\n", "\n", $text);
	$text = str_replace("\n", "</p><p>", $text);
	$text = str_replace('<!-- w --><a class="postlink"', '<a', $text);
	$text = str_replace('<!-- m --><a class="postlink"', '<a', $text);
	$text = str_replace('<!-- l --><a class="postlink-local"', '<a', $text);
	$text = str_replace('</a><!-- w -->', '</a>', $text);
	$text = str_replace('</a><!-- m -->', '</a>', $text);
	$text = str_replace('</a><!-- l -->', '</a>', $text);
	$text = str_replace('<!-- e -->', '', $text);
	$text = preg_replace_callback('/<!-- ia[0-9]+ -->([^<]+)<!-- ia[0-9]+ -->/', 'sanitize_attachments', $text);
	return '<p>' . $text . '</p>';
}

/**
	* Does second-pass bbencoding. This should be used before displaying the message in
	* a thread. Assumes the message is already first-pass encoded, and we are given the
	* correct UID as used in first-pass encoding.
	*/
function bbencode_second_pass($text, $uid) {
	global $bbcode_tpl;
	// I set these as the minimum set which is both XHTML 1.1 compliant and bbPress supported.
	// I don't want the imported texts to be have any visual richness than can't be obtained from bbPress default post function.
	$templates = <<<TEMPLATES
<!-- BEGIN ulist_open --><ul><!-- END ulist_open -->
<!-- BEGIN ulist_close --></ul><!-- END ulist_close -->

<!-- BEGIN olist_open --><ol type="{LIST_TYPE}"><!-- END olist_open -->
<!-- BEGIN olist_close --></ol><!-- END olist_close -->

<!-- BEGIN listitem_open --><li><!-- END listitem_open -->
<!-- BEGIN listitem_close --></li><!-- END listitem_close -->

<!-- BEGIN quote_username_open --><p>{USERNAME} {L_WROTE}:</p><blockquote><!-- END quote_username_open -->
<!-- BEGIN quote_open --><blockquote><!-- END quote_open -->
<!-- BEGIN quote_close --></blockquote><!-- END quote_close -->

<!-- BEGIN code_open --><p><code><!-- END code_open -->
<!-- BEGIN code_close --></code></p><!-- END code_close -->

<!-- BEGIN b_open --><strong><!-- END b_open -->
<!-- BEGIN b_close --></strong><!-- END b_close -->

<!-- BEGIN u_open --><em><!-- END u_open -->
<!-- BEGIN u_close --></em><!-- END u_close -->

<!-- BEGIN i_open --><em><!-- END i_open -->
<!-- BEGIN i_close --></em><!-- END i_close -->

<!-- BEGIN color_open --><!-- END color_open -->
<!-- BEGIN color_close --><!-- END color_close -->

<!-- BEGIN size_open --><!-- END size_open -->
<!-- BEGIN size_close --><!-- END size_close -->

<!-- BEGIN img --><img src="{URL}" alt="" /><!-- END img -->

<!-- BEGIN url --><a href="{URL}">{DESCRIPTION}</a><!-- END url -->

<!-- BEGIN email --><a href="mailto:{EMAIL}" class="email">{EMAIL}</a><!-- END email -->
TEMPLATES;
	$text = preg_replace('#(script|about|applet|activex|chrome):#is', "\\1&#058;", $text);

	// pad it with a space so we can distinguish between FALSE and matching the 1st char (index 0).
	// This is important; bbencode_quote(), bbencode_list(), and bbencode_code() all depend on it.
	$text = " " . $text;

	// First: If there isn't a "[" and a "]" in the message, don't bother.
	if(!(strpos($text, "[") && strpos($text, "]")) ) {
		// Remove padding, return.
		$text = substr($text, 1);
		return $text;
	}

	// Only load the templates ONCE..
	if(!defined("BBCODE_TPL_READY")) {
		$tpl = $templates;

		// replace \ with \\ and then ' with \'.
		$tpl = str_replace('\\', '\\\\', $tpl);
		$tpl  = str_replace('\'', '\\\'', $tpl);

		// strip newlines.
		$tpl  = str_replace("\n", '', $tpl);

		// Turn template blocks into PHP assignment statements for the values of $bbcode_tpls..
		$tpl = preg_replace('#<!-- BEGIN (.*?) -->(.*?)<!-- END (.*?) -->#', "\n" . '$bbcode_tpls[\'\\1\'] = \'\\2\';', $tpl);

		$bbcode_tpls = array();
		eval($tpl);
		$bbcode_tpl = $bbcode_tpls;

		$bbcode_tpl['olist_open'] = str_replace('{LIST_TYPE}', '\\1', $bbcode_tpl['olist_open']);
		$bbcode_tpl['color_open'] = str_replace('{COLOR}', '\\1', $bbcode_tpl['color_open']);
		$bbcode_tpl['size_open'] = str_replace('{SIZE}', '\\1', $bbcode_tpl['size_open']);
		$bbcode_tpl['quote_open'] = str_replace('{L_QUOTE}', utf8_decode(CMSL_FIELD_QUOTE), $bbcode_tpl['quote_open']);
		$bbcode_tpl['quote_username_open'] = str_replace('{L_QUOTE}', utf8_decode(CMSL_FIELD_QUOTE), $bbcode_tpl['quote_username_open']);
		$bbcode_tpl['quote_username_open'] = str_replace('{L_WROTE}', utf8_decode(CMSL_MSG_WROTE), $bbcode_tpl['quote_username_open']);
		$bbcode_tpl['quote_username_open'] = str_replace('{USERNAME}', '\\1', $bbcode_tpl['quote_username_open']);
		$bbcode_tpl['code_open'] = str_replace('{L_CODE}', utf8_decode(CMSL_FIELD_CODE), $bbcode_tpl['code_open']);
		$bbcode_tpl['img'] = str_replace('{URL}', '\\1', $bbcode_tpl['img']);

		// URLs are done in several different ways.
		$bbcode_tpl['url1'] = str_replace('{URL}', '\\1', $bbcode_tpl['url']);
		$bbcode_tpl['url1'] = str_replace('{DESCRIPTION}', '\\1', $bbcode_tpl['url1']);
		$bbcode_tpl['url2'] = str_replace('{URL}', 'http://\\1', $bbcode_tpl['url']);
		$bbcode_tpl['url2'] = str_replace('{DESCRIPTION}', '\\1', $bbcode_tpl['url2']);
		$bbcode_tpl['url3'] = str_replace('{URL}', '\\1', $bbcode_tpl['url']);
		$bbcode_tpl['url3'] = str_replace('{DESCRIPTION}', '\\2', $bbcode_tpl['url3']);
		$bbcode_tpl['url4'] = str_replace('{URL}', 'http://\\1', $bbcode_tpl['url']);
		$bbcode_tpl['url4'] = str_replace('{DESCRIPTION}', '\\3', $bbcode_tpl['url4']);
		$bbcode_tpl['email'] = str_replace('{EMAIL}', '\\1', $bbcode_tpl['email']);

		define('BBCODE_TPL_READY', true);
	}

	// [CODE] and [/CODE] for posting code (HTML, PHP, C etc etc) in your posts.
	$text = bbencode_second_pass_code($text, $uid, $bbcode_tpl);

	// [QUOTE] and [/QUOTE] for posting replies with quote, or just for quoting stuff.
	$text = str_replace("[quote:$uid]", $bbcode_tpl['quote_open'], $text);
	$text = str_replace("[/quote:$uid]", $bbcode_tpl['quote_close'], $text);

	// New one liner to deal with opening quotes with usernames...
	// replaces the two line version that I had here before..
	$text = preg_replace("/\[quote(?::$uid)?=(?:\"|&quot;)(.*?)(?:\"|&quot;).*?\]/si", $bbcode_tpl['quote_username_open'], $text);

	// [list] and [list=x] for(un)ordered lists.
	// unordered lists
	$text = str_replace("[list:$uid]", $bbcode_tpl['ulist_open'], $text);
	// li tags
	$text = str_replace("[*:$uid]", $bbcode_tpl['listitem_open'], $text);
	$text = str_replace("[/*:$uid]", $bbcode_tpl['listitem_close'], $text);
	$text = str_replace("[/*:m:$uid]", $bbcode_tpl['listitem_close'], $text);
	// ending tags
	$text = str_replace("[/list:u:$uid]", $bbcode_tpl['ulist_close'], $text);
	$text = str_replace("[/list:o:$uid]", $bbcode_tpl['olist_close'], $text);
	// Ordered lists
	$text = preg_replace("/\[list=([a1]):$uid\]/si", $bbcode_tpl['olist_open'], $text);

	// colours
	$text = preg_replace("/\[color=(\#[0-9A-F]{6}|[a-z]+):$uid\]/si", $bbcode_tpl['color_open'], $text);
	$text = str_replace("[/color:$uid]", $bbcode_tpl['color_close'], $text);

	// size
	$text = preg_replace("/\[size=([1-2]?[0-9]):$uid\]/si", $bbcode_tpl['size_open'], $text);
	$text = str_replace("[/size:$uid]", $bbcode_tpl['size_close'], $text);

	// [b] and [/b] for bolding text.
	$text = str_replace("[b:$uid]", $bbcode_tpl['b_open'], $text);
	$text = str_replace("[/b:$uid]", $bbcode_tpl['b_close'], $text);

	// [u] and [/u] for underlining text.
	$text = str_replace("[u:$uid]", $bbcode_tpl['u_open'], $text);
	$text = str_replace("[/u:$uid]", $bbcode_tpl['u_close'], $text);

	// [i] and [/i] for italicizing text.
	$text = str_replace("[i:$uid]", $bbcode_tpl['i_open'], $text);
	$text = str_replace("[/i:$uid]", $bbcode_tpl['i_close'], $text);

	// Patterns and replacements for URL and email tags..
	$patterns = array();
	$replacements = array();

	// [img]image_url_here[/img] code..
	// This one gets first-passed..
	$patterns[] = "#\[img:$uid\]([^?].*?)\[/img:$uid\]#i";
	$replacements[] = $bbcode_tpl['img'];

	$patterns[] = "#\[attachment=[^\]]+\]([^?].*?)\[/attachment:[^\]]+\]#i";
	$replacements[] = $bbcode_tpl['img'];

	// [url]www.phpbb.com[/url] code.. (no xxxx:// prefix).
	$patterns[] = "#\[url(?::.+)?\]((www|ftp)\..*?)\[/url(?::.+)?\]#is";
	$replacements[] = $bbcode_tpl['url2'];

	// matches a [url]xxxx://www.phpbb.com[/url] code..
	$patterns[] = "#\[url(?::.+)?\](.*?)\[/url(?::.+)?\]#is";
	$replacements[] = $bbcode_tpl['url1'];

	// [url=www.phpbb.com]phpBB[/url] code.. (no xxxx:// prefix).
	$patterns[] = "#\[url=((www|ftp)\..*?)(?::.+)?\](.*?)\[/url(?::.+)?\]#is";
	$replacements[] = $bbcode_tpl['url4'];

	// [url=xxxx://www.phpbb.com]phpBB[/url] code..
	$patterns[] = "#\[url=(.*?)(?::.+)?\](.*?)\[/url(?::.+)?\]#is";
	$replacements[] = $bbcode_tpl['url3'];

	// [email]user@domain.tld[/email] code..
	$patterns[] = "#\[email\]([a-z0-9&\-_.]+?@[\w\-]+\.([\w\-\.]+\.)?[\w]+)\[/email\]#si";
	$replacements[] = $bbcode_tpl['email'];

	$text = preg_replace($patterns, $replacements, $text);

	// Remove our padding from the string..
	$text = substr($text, 1);

	return $text;
}

function bbencode_first_pass($text, $uid) {
	// pad it with a space so we can distinguish between FALSE and matching the 1st char (index 0).
	// This is important; bbencode_quote(), bbencode_list(), and bbencode_code() all depend on it.
	$text = " " . $text;

	// [CODE] and [/CODE] for posting code (HTML, PHP, C etc etc) in your posts.
	$text = bbencode_first_pass_pda($text, $uid, '[code]', '[/code]', '', true, '');

	// [QUOTE] and [/QUOTE] for posting replies with quote, or just for quoting stuff.
	$text = bbencode_first_pass_pda($text, $uid, '[quote]', '[/quote]', '', false, '');
	$text = bbencode_first_pass_pda($text, $uid, '/\[quote=(\\\".*?\\\")\]/is', '[/quote]', '', false, '', "[quote:$uid=\\1]");

	// [list] and [list=x] for(un)ordered lists.
	$open_tag = array();
	$open_tag[0] = "[list]";

	// unordered..
	$text = bbencode_first_pass_pda($text, $uid, $open_tag, "[/list]", "[/list:u]", false, 'replace_listitems');

	$open_tag[0] = "[list=1]";
	$open_tag[1] = "[list=a]";

	// ordered.
	$text = bbencode_first_pass_pda($text, $uid, $open_tag, "[/list]", "[/list:o]",  false, 'replace_listitems');

	// [color] and [/color] for setting text color
	$text = preg_replace("#\[color=(\#[0-9A-F]{6}|[a-z\-]+)\](.*?)\[/color\]#si", "[color=\\1:$uid]\\2[/color:$uid]", $text);

	// [size] and [/size] for setting text size
	$text = preg_replace("#\[size=([1-2]?[0-9])\](.*?)\[/size\]#si", "[size=\\1:$uid]\\2[/size:$uid]", $text);

	// [b] and [/b] for bolding text.
	$text = preg_replace("#\[b\](.*?)\[/b\]#si", "[b:$uid]\\1[/b:$uid]", $text);

	// [u] and [/u] for underlining text.
	$text = preg_replace("#\[u\](.*?)\[/u\]#si", "[u:$uid]\\1[/u:$uid]", $text);

	// [i] and [/i] for italicizing text.
	$text = preg_replace("#\[i\](.*?)\[/i\]#si", "[i:$uid]\\1[/i:$uid]", $text);

	// [img]image_url_here[/img] code..
	$text = preg_replace("#\[img\]((http|ftp|https|ftps)://)([^ \?&=\#\"\n\r\t<]*?(\.(jpg|jpeg|gif|png)))\[/img\]#sie", "'[img:$uid]\\1' . str_replace(' ', '%20', '\\3') . '[/img:$uid]'", $text);

	// Remove our padding from the string.
	return substr($text, 1);;
}

/**
	* $text - The text to operate on.
	* $uid - The UID to add to matching tags.
	* $open_tag - The opening tag to match. Can be an array of opening tags.
	* $close_tag - The closing tag to match.
	* $close_tag_new - The closing tag to replace with.
	* $mark_lowest_level - boolean - should we specially mark the tags that occur
*                                      at the lowest level of nesting? (useful for [code], because
	*                                              we need to match these tags first and transform HTML tags
	*                                              in their contents..
	* $func - This variable should contain a string that is the name of a function.
	*                              That function will be called when a match is found, and passed 2
	*                              parameters: ($text, $uid). The function should return a string.
	*                              This is used when some transformation needs to be applied to the
	*                              text INSIDE a pair of matching tags. If this variable is FALSE or the
	*                              empty string, it will not be executed.
	* If open_tag is an array, then the pda will try to match pairs consisting of
	* any element of open_tag followed by close_tag. This allows us to match things
	* like [list=A]...[/list] and [list=1]...[/list] in one pass of the PDA.
	*
	* NOTES:       - this function assumes the first character of $text is a space.
	*              - every opening tag and closing tag must be of the [...] format.
	*/
function bbencode_first_pass_pda($text, $uid, $open_tag, $close_tag, $close_tag_new, $mark_lowest_level, $func, $open_regexp_replace = false) {
	$open_tag_count = 0;

	if(!$close_tag_new || ($close_tag_new == ''))
		$close_tag_new = $close_tag;

	$close_tag_length = strlen($close_tag);
	$close_tag_new_length = strlen($close_tag_new);
	$uid_length = strlen($uid);
	$use_function_pointer = ($func && ($func != ''));
	$stack = array();

	if(is_array($open_tag)) {
		if(0 == count($open_tag)) {
			// No opening tags to match, so return.
			return $text;
		}
		$open_tag_count = count($open_tag);
	}
	else {
		// only one opening tag. make it into a 1-element array.
		$open_tag_temp = $open_tag;
		$open_tag = array();
		$open_tag[0] = $open_tag_temp;
		$open_tag_count = 1;
	}

	$open_is_regexp = false;

	if($open_regexp_replace) {
		$open_is_regexp = true;
		if(!is_array($open_regexp_replace)) {
			$open_regexp_temp = $open_regexp_replace;
			$open_regexp_replace = array();
			$open_regexp_replace[0] = $open_regexp_temp;
		}
	}

	if($mark_lowest_level && $open_is_regexp) {
		die('Unsupported operation for bbcode_first_pass_pda().');
	}

	// Start at the 2nd char of the string, looking for opening tags.
	$curr_pos = 1;
	while($curr_pos && ($curr_pos < strlen($text))) {
		$curr_pos = strpos($text, "[", $curr_pos);

		// If not found, $curr_pos will be 0, and the loop will end.
		if($curr_pos) {
			// We found a [. It starts at $curr_pos.
			// check if it's a starting or ending tag.
			$found_start = false;
			$which_start_tag = "";
			$start_tag_index = -1;

			for($i = 0; $i < $open_tag_count; $i++) {
				// Grab everything until the first "]"...
				$possible_start = substr($text, $curr_pos, strpos($text, ']', $curr_pos + 1) - $curr_pos + 1);

				// We're going to try and catch usernames with "[' characters.
				if( preg_match('#\[quote=\\\"#si', $possible_start, $match) && !preg_match('#\[quote=\\\"(.*?)\\\"\]#si', $possible_start) ) {
					// OK we are in a quote tag that probably contains a ] bracket.
					// Grab a bit more of the string to hopefully get all of it..
					if($close_pos = strpos($text, '"]', $curr_pos + 9)) {
						if(strpos(substr($text, $curr_pos + 9, $close_pos - ($curr_pos + 9)), '[quote') === false) {
							$possible_start = substr($text, $curr_pos, $close_pos - $curr_pos + 2);
						}
					}
				}

				// Now compare, either using regexp or not.
				if($open_is_regexp) {
					$match_result = array();
					if(preg_match($open_tag[$i], $possible_start, $match_result)) {
						$found_start = true;
						$which_start_tag = $match_result[0];
						$start_tag_index = $i;
						break;
					}
				}
				else {
					// straightforward string comparison.
					if(0 == strcasecmp($open_tag[$i], $possible_start)) {
						$found_start = true;
						$which_start_tag = $open_tag[$i];
						$start_tag_index = $i;
						break;
					}
				}
			}

			if($found_start) {
				// We have an opening tag.
				// Push its position, the text we matched, and its index in the open_tag array on to the stack, and then keep going to the right.
				$match = array("pos" => $curr_pos, "tag" => $which_start_tag, "index" => $start_tag_index);
				array_push($stack, $match);
				// Rather than just increment $curr_pos
				// Set it to the ending of the tag we just found
				// Keeps error in nested tag from breaking out
				// of table structure..
				$curr_pos += strlen($possible_start);
			} else {
				// check for a closing tag..
				$possible_end = substr($text, $curr_pos, $close_tag_length);
				if(0 == strcasecmp($close_tag, $possible_end)) {
					// We have an ending tag.
					// Check if we've already found a matching starting tag.
					if(sizeof($stack) > 0) {
						// There exists a starting tag.
						$curr_nesting_depth = sizeof($stack);
						// We need to do 2 replacements now.
						$match = array_pop($stack);
						$start_index = $match['pos'];
						$start_tag = $match['tag'];
						$start_length = strlen($start_tag);
						$start_tag_index = $match['index'];

						if($open_is_regexp) {
							$start_tag = preg_replace($open_tag[$start_tag_index], $open_regexp_replace[$start_tag_index], $start_tag);
						}

						// everything before the opening tag.
						$before_start_tag = substr($text, 0, $start_index);

						// everything after the opening tag, but before the closing tag.
						$between_tags = substr($text, $start_index + $start_length, $curr_pos - $start_index - $start_length);

						// Run the given function on the text between the tags..
						if($use_function_pointer) {
							$between_tags = $func($between_tags, $uid);
						}

						// everything after the closing tag.
						$after_end_tag = substr($text, $curr_pos + $close_tag_length);

						// Mark the lowest nesting level if needed.
						if($mark_lowest_level && ($curr_nesting_depth == 1)) {
							if($open_tag[0] == '[code]') {
								$code_entities_match = array('#<#', '#>#', '#"#', '#:#', '#\[#', '#\]#', '#\(#', '#\)#', '#\{#', '#\}#');
								$code_entities_replace = array('&lt;', '&gt;', '&quot;', '&#58;', '&#91;', '&#93;', '&#40;', '&#41;', '&#123;', '&#125;');
								$between_tags = preg_replace($code_entities_match, $code_entities_replace, $between_tags);
							}
							$text = $before_start_tag . substr($start_tag, 0, $start_length - 1) . ":$curr_nesting_depth:$uid]";
							$text .= $between_tags . substr($close_tag_new, 0, $close_tag_new_length - 1) . ":$curr_nesting_depth:$uid]";
						}
						else {
							if($open_tag[0] == '[code]') {
								$text = $before_start_tag . '&#91;code&#93;';
								$text .= $between_tags . '&#91;/code&#93;';
							}
							else {
								if($open_is_regexp) {
									$text = $before_start_tag . $start_tag;
								}
								else {
									$text = $before_start_tag . substr($start_tag, 0, $start_length - 1) . ":$uid]";
								}
								$text .= $between_tags . substr($close_tag_new, 0, $close_tag_new_length - 1) . ":$uid]";
							}
						}

						$text .= $after_end_tag;

						// Now.. we've screwed up the indices by changing the length of the string.
						// So, if there's anything in the stack, we want to resume searching just after it.
						// otherwise, we go back to the start.
						if(sizeof($stack) > 0) {
							$match = array_pop($stack);
							$curr_pos = $match['pos'];
						}
						else {
							$curr_pos = 1;
						}
					}
					else {
						// No matching start tag found. Increment pos, keep going.
						++$curr_pos;
					}
				}
				else {
					// No starting tag or ending tag.. Increment pos, keep looping.,
					++$curr_pos;
				}
			}
		}
	} // while

	return $text;
}

/**
	* Does second-pass bbencoding of the [code] tags. This includes
	* running htmlspecialchars() over the text contained between
	* any pair of [code] tags that are at the first level of
	* nesting. Tags at the first level of nesting are indicated
	* by this format: [code:1:$uid] ... [/code:1:$uid]
	* Other tags are in this format: [code:$uid] ... [/code:$uid]
	*/
function bbencode_second_pass_code($text, $uid, $bbcode_tpl) {
	$code_start_html = $bbcode_tpl['code_open'];
	$code_end_html =  $bbcode_tpl['code_close'];

	// First, do all the 1st-level matches. These need an htmlspecialchars() run,
	// so they have to be handled differently.
	$match_count = preg_match_all("#\[code:1:$uid\](.*?)\[/code:1:$uid\]#si", $text, $matches);

	for($i = 0; $i < $match_count; $i++) {
		$before_replace = $matches[1][$i];
		$after_replace = $matches[1][$i];

		// Replace 2 spaces with "&nbsp; " so non-tabbed code indents without making huge long lines.
		$after_replace = str_replace("  ", "&nbsp; ", $after_replace);
		// now Replace 2 spaces with " &nbsp;" to catch odd #s of spaces.
		$after_replace = str_replace("  ", " &nbsp;", $after_replace);

		// Replace tabs with "&nbsp; &nbsp;" so tabbed code indents sorta right without making huge long lines.
		$after_replace = str_replace("\t", "&nbsp; &nbsp;", $after_replace);

		// now Replace space occurring at the beginning of a line
		$after_replace = preg_replace("/^ {1}/m", '&nbsp;', $after_replace);

		$str_to_match = "[code:1:$uid]" . $before_replace . "[/code:1:$uid]";

		$replacement = $code_start_html.$after_replace.$code_end_html;
		$text = str_replace($str_to_match, $replacement, $text);
	}

	// Now, do all the non-first-level matches. These are simple.
	$text = str_replace("[code:$uid]", $code_start_html, $text);
	$text = str_replace("[/code:$uid]", $code_end_html, $text);

	return $text;
}

/**
	* This is used to change a [*] tag into a [*:$uid] tag as part
	* of the first-pass bbencoding of [list] tags. It fits the
	* standard required in order to be passed as a variable
	* function into bbencode_first_pass_pda().
	*/
function replace_listitems($text, $uid) {
	return str_replace("[*]", "[*:$uid]", $text);
}

function smilies_pass($message) {
	$smilies = array(
	  ':lol:'     => 'X-D',
	  ':shock:'   => '8-O',
	  ':oops:'    => ':-//',
	  ':cry:'     => ':\'(',
	  ':wink:'    => ';-)',
	  ':roll:'    => '',
	  ':twisted:' => '}:-)',
	  ':evil:'    => '}:-@',
	  ':arrow:'   => '->',
	  ':mrgreen:' => ':-D',
	  ':green:'   => ':-D',
	  ':smile:'   => ':-)',
	  ':mad:'     => ':-x',
	  ':neutral:' => ':-|',
	  ':razz:'    => ':-P',
	  ':cool:'    => '8-)',
	  ':sad:'     => ':-(',
	  ':eek:'     => ':-O',
	  ':idea:'    => '',
	  ':|'        => ':-|',
	  ':!:'       => '(!)',
	  ':!!!:'     => '(!)',
	  ':?:'       => ':-?',
	  ':???:'     => ':-?'
	);
	$message = preg_replace('/<!--.*?{SMILIES_PATH}\/icon(?:_e)?_([^.]+)\..*?-->/', ':\1:', $message);
	$orig = $repl = array();
	foreach ($smilies as $smily => $translation) {
		$orig[] = "/(?<=.\W|\W.|^\W)" . str_replace('/', '\\'.'/', preg_quote($smily))."(?=.\W|\W.|\W$)/";
		$repl[] = $translation;
	}

	if(count($orig))
		$message = substr(preg_replace($orig, $repl, ' ' . $message . ' '), 1, -1);

	return $message;
}

function make_slug($text) {
	global $slugs;
	$text = cms_sanitize($text);
	if(isset($slugs[$text])) {
		$slugs[$text] ++;
		return $text . '-' . $slugs[$text];
	} else {
		$slugs[$text] = 1;
	}
	return $text;
}

function clear_slugs() {
	global $slugs;
	if(isset($slugs)) unset($slugs);
}

?>
