<?php /* This file was automatically generated by make.php. DO NOT EDIT. */ ?>

<?php
define('VERSION', '1.2.0');
/**
 * Vanilla 2 Exporter
 * This script exports other forum databases to the Vanilla 2 import format.
 * To use this script copy it to your web server and open it in your browser.
 * If you have a larger database the directory should be writable so that the export file can be saved locally and zipped.
 *
 * Copyright 2010 Vanilla Forums Inc.
 * This file is part of Garden.
 * Garden is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * Garden is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with Garden.  If not, see <http://www.gnu.org/licenses/>.
 * Contact Vanilla Forums Inc. at support [at] vanillaforums [dot] com
 *
 * @package VanillaPorter
 */

if(defined('DEBUG'))
   error_reporting(E_ALL);
else
   error_reporting(E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR);
ini_set('display_errors', 'on');
ini_set('track_errors', 1);

$e = new SMF();
$e->DoExport();

die ("OK");

function url_image_size($url)
{
	global $sourcedir;

	$t = microtime();

	// Get the host to pester...
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);

	// Can't figure it out, just try the image size.
	if ($url == '' || $url == 'http://' || $url == 'https://')
		return false;
	elseif (!isset($match[1]))
		$size = @getimagesize($url);
	else
	{
		// Try to connect to the server... give it half a second.
		$temp = 0;
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);

		// Successful?  Continue...
		if ($fp != false)
		{
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
			fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'User-Agent: PHP/SMF' . "\r\n" . 'Connection: close' . "\r\n\r\n");

			// Read in the HTTP/1.1 or whatever.
			$test = substr(fgets($fp, 11), -1);
			fclose($fp);

			// See if it returned a 404/403 or something.
			if ($test < 4)
			{
				$size = @getimagesize($url);

				// This probably means allow_url_fopen is off, let's try GD.
				if ($size === false && function_exists('imagecreatefromstring'))
				{
					include_once($sourcedir . '/Subs-Package.php');

					// It's going to hate us for doing this, but another request...
					$image = @imagecreatefromstring(fetch_web_data($url));
					if ($image !== false)
					{
						$size = array(imagesx($image), imagesy($image));
						imagedestroy($image);
					}
				}
			}
		}
	}

	// If we didn't get it, we failed.
	if (!isset($size))
		$size = false;

    // // Didn't work.
	return $size;
}
 
global $Supported;

/** @var array Supported forum packages: classname => array(name, prefix) */
$Supported = array(
   'vanilla1' => array('name'=> 'Vanilla 1.*', 'prefix'=>'LUM_'),
   'vbulletin' => array('name'=>'vBulletin 3.*', 'prefix'=>'vb_'),
   'phpbb' => array('name'=>'phpBB 3.*', 'prefix' => 'phpbb_'),
   'bbPress' => array('name'=>'bbPress 1.*', 'prefx' => 'bb_'),
   'SMF' => array('name'=>'SMF 1.*', 'prefx' => 'smf_')   
);

// Support Files

/* Contents included from class.exportmodel.php */
?><?php
/**
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */

/**
 * Object for exporting other database structures into a format that can be imported.
 */
class ExportModel {
   const COMMENT = '//';
   const DELIM = ',';
   const ESCAPE = '\\';
   const NEWLINE = "\n";
   const NULL = '\N';
   const QUOTE = '"';


   /** @var array Any comments that have been written during the export. */
   public $Comments = array();

   /** @var string The charcter set to set as the connection anytime the database connects. */
   public $CharacterSet = 'utf8';

   /** @var object File pointer */
   protected $_File = NULL;

   /** @var string A prefix to put into an automatically generated filename. */
   public $FilenamePrefix = '';

   protected $_Host;

   protected $_Limit = 20000;

   /** @var object PDO instance */
   protected $_PDO = NULL;

   protected $_Password;

   /** @var string The path to the export file. */
   public $Path = '';

   /**
    * @var string The database prefix. When you pass a sql string to ExportTable() it will replace occurances of :_ with this property.
    * @see ExportModel::ExportTable()
    */
   public $Prefix = '';

   /**
    * @var array Strucutes that define the format of the export tables.
    */
   protected $_Structures = array(
      'Activity' => array(
            'ActivityUserID' => 'int',
            'RegardingUserID' => 'int',
            'Story' => 'text',
            'InsertUserID' => 'int',
            'DateInserted' => 'datetime'),
      'Category' => array(
            'CategoryID' => 'int',
            'Name' => 'varchar(30)',
            'Description' => 'varchar(250)',
            'ParentCategoryID' => 'int',
            'DateInserted' => 'datetime',
            'InsertUserID' => 'int',
            'DateUpdated' => 'datetime',
            'UpdateUserID' => 'int',
            'Sort' => 'int'),
      'Comment' => array(
            'CommentID' => 'int',
            'DiscussionID' => 'int',
            'DateInserted' => 'datetime',
            'InsertUserID' => 'int',
            'DateUpdated' => 'datetime',
            'UpdateUserID' => 'int',
            'Format' => 'varchar(20)',
            'Body' => 'text',
            'Score' => 'float'),
      'Conversation' => array(
            'ConversationID' => 'int',
            'FirstMessageID' => 'int',
            'DateInserted' => 'datetime',
            'InsertUserID' => 'int',
            'DateUpdated' => 'datetime',
            'UpdateUserID' => 'int'),
      'ConversationMessage' => array(
            'MessageID' => 'int',
            'ConversationID' => 'int',
            'Body' => 'text',
            'InsertUserID' => 'int',
            'DateInserted' => 'datetime'),
      'Discussion' => array(
            'DiscussionID' => 'int',
            'Name' => 'varchar(100)',
            'Body' => 'text',
            'Format' => 'varchar(20)',
            'CategoryID' => 'int',
            'DateInserted' => 'datetime',
            'InsertUserID' => 'int',
            'DateUpdated' => 'datetime',
            'UpdateUserID' => 'int',
            'DateLastComment' => 'datetime',
            'CountComments' => 'int',
            'Score' => 'float',
            'Closed' => 'tinyint',
            'Announce' => 'tinyint',
            'Sink' => 'tinyint'),
      'Role' => array(
            'RoleID' => 'int',
            'Name' => 'varchar(100)',
            'Description' => 'varchar(200)',
            'CanSession' => 'tinyint'),
      'User' => array(
            'UserID' => 'int',
            'Name' => 'varchar(20)',
            'Email' => 'varchar(200)',
            'Password' => 'varbinary(34)',
            //'Gender' => array('m', 'f'),
            'Score' => 'float',
            'InviteUserID' => 'int',
            'HourOffset' => 'int',
            'CountDiscussions' => 'int',
            'CountComments' => 'int',
            'Photo' => 'varchar(255)',
            'DateOfBirth' => 'datetime',
            'DateFirstVisit' => 'datetime',
            'DateLastActive' => 'datetime',
            'DateInserted' => 'datetime',
            'DateUpdated' => 'datetime'),
      'UserConversation' => array(
            'UserID' => 'int',
            'ConversationID' => 'int',
            'LastMessageID' => 'int'),
      'UserDiscussion' => array(
            'UserID' => 'int',
            'DiscussionID' => 'int',
            'Bookmarked' => 'tinyint',
            'DateLastViewed' => 'datetime',
            'CountComments' => 'int'),
      'UserMeta' => array(
            'UserID' => 'int',
            'Name' => 'varchar(255)',
            'Value' => 'text'),
      'UserRole' => array(
            'UserID' => 'int',
            'RoleID' => 'int')
   );

   /**
    * @var bool Whether or not to use compression when creating the file.
    */
   protected $_UseCompression = TRUE;

   protected $_Username;

   /**
    *
    * @var bool Whether or not to stream the export the the output rather than save a file.
    */
   public $UseStreaming = FALSE;


   /**
    * Create the export file and begin the export.
    * @param string $Path The path to the export file.
    * @param string $Source The source program that created the export. This may be used by the import routine to do additional processing.
    */
   public function BeginExport($Path = '', $Source = '', $Header = array()) {
      $this->Comments = array();
      $this->BeginTime = microtime(TRUE);

      if($Path)
         $this->Path = $Path;
      if(!$this->Path)
         $this->Path = 'export_'.($this->FilenamePrefix ? $this->FilenamePrefix.'_' : '').date('Y-m-d_His').'.txt'.($this->UseCompression() ? '.gz' : '');

      $fp = $this->_OpenFile();

      fwrite($fp, 'Vanilla Export: '.$this->Version());
      if($Source)
         fwrite($fp, self::DELIM.' Source: '.$Source);
      foreach ($Header as $Key => $Value) {
         fwrite($fp, self::DELIM." $Key: $Value");
      }
      fwrite($fp, self::NEWLINE.self::NEWLINE);
      $this->Comment('Export Started: '.date('Y-m-d H:i:s'));
   }

   /**
    * Write a comment to the export file.
    * @param string $Message The message to write.
    * @param bool $Echo Whether or not to echo the message in addition to writing it to the file.
    */
   public function Comment($Message, $Echo = TRUE) {
      fwrite($this->_File, self::COMMENT.' '.str_replace(self::NEWLINE, self::NEWLINE.self::COMMENT.' ', $Message).self::NEWLINE);
      if($Echo)
         $this->Comments[] = $Message;
   }

   /**
    * End the export and close the export file. This method must be called if BeginExport() has been called or else the export file will not be closed.
    */
   public function EndExport() {
      $this->EndTime = microtime(TRUE);
      $this->TotalTime = $this->EndTime - $this->BeginTime;

      $this->Comment('Export Completed: '.date('Y-m-d H:i:s'));
      $this->Comment(sprintf('Elapsed Time: %s', self::FormatElapsed($this->TotalTime)));

      if($this->UseStreaming) {
         //ob_flush();
      } else {
         if($this->UseCompression() && function_exists('gzopen'))
            gzclose($this->_File);
         else
            fclose($this->_File);
      }
   }

   /**
    * Export a table to the export file.
    * @param string $TableName the name of the table to export. This must correspond to one of the accepted Vanilla tables.
    * @param mixed $Query The query that will fetch the data for the export this can be one of the following:
    *  - <b>String</b>: Represents a string of SQL to execute.
    *  - <b>PDOStatement</b>: Represents an already executed query result set.
    *  - <b>Array</b>: Represents an array of associative arrays or objects containing the data in the export.
    *  @param array $Mappings Specifies mappings, if any, between the source and the export where the keys represent the source columns and the values represent Vanilla columns.
    *	  - If you specify a Vanilla column then it must be in the export structure contained in this class.
    *   - If you specify a MySQL type then the column will be added.
    *   - If you specify an array you can have the following keys: Column, and Type where Column represents the new column name and Type represents the MySQL type.
    *  For a list of the export tables and columns see $this->Structure().
    */
   public function ExportTable($TableName, $Query, $Mappings = array()) {
      $BeginTime = microtime(TRUE);

      $RowCount = $this->_ExportTable($TableName, $Query, $Mappings);

      $EndTime = microtime(TRUE);
      $Elapsed = self::FormatElapsed($BeginTime, $EndTime);
      $this->Comment("Exported Table: $TableName ($RowCount rows, $Elapsed)");
      fwrite($this->_File, self::NEWLINE);
   }

   protected function _ExportTable($TableName, $Query, $Mappings = array()) {
      $fp = $this->_File;

      // Make sure the table is valid for export.
      if (!array_key_exists($TableName, $this->_Structures)) {
         $this->Comment("Error: $TableName is not a valid export."
            ." The valid tables for export are ". implode(", ", array_keys($this->_Structures)));
         fwrite($fp, self::NEWLINE);
         return;
      }
      $Structure = $this->_Structures[$TableName];

      // Set the search and replace to escape strings.
      $EscapeSearch = array(self::ESCAPE, self::DELIM, self::NEWLINE, self::QUOTE); // escape must go first
      $EscapeReplace = array(self::ESCAPE.self::ESCAPE, self::ESCAPE.self::DELIM, self::ESCAPE.self::NEWLINE, self::ESCAPE.self::QUOTE);

      $LastID = 0;
      $IDName = 'NOTSET';
      $FirstQuery = TRUE;

      // Get the filters from the mappings.
      $Filters = array();
      foreach ($Mappings as $Column => $Mapping) {
         if (is_array($Mapping) &&isset($Mapping['Column']) && isset($Mapping['Filter'])) {
            $Filters[$Mapping['Column']] = $Mapping['Filter'];
         }
      }

      $Data = $this->Query($Query, $IDName, $LastID, $this->_Limit);
      $Mb = function_exists('mb_detect_encoding');

      // Loop through the data and write it to the file.
      $RowCount = 0;
      if ($Data !== FALSE) {
         while (($Row = mysql_fetch_assoc($Data)) !== FALSE) {
            $Row = (array)$Row; // export%202010-05-06%20210937.txt
            $RowCount++;
            if($FirstQuery) {
               // Start with the table name.
               fwrite($fp, 'Table: '.$TableName.self::NEWLINE);

               // Get the export structure.
               $ExportStructure = $this->GetExportStructure($Row, $Structure, $Mappings);

               // Build and write the table header.
               $TableHeader = $this->_GetTableHeader($ExportStructure, $Structure);

               fwrite($fp, $TableHeader.self::NEWLINE);

               $Mappings = array_flip($Mappings);

               $FirstQuery = FALSE;
            }

            $First = TRUE;

            // Loop through the columns in the export structure and grab their values from the row.
            $ExRow = array();
            foreach ($ExportStructure as $Field => $Type) {
               // Get the value of the export.
               if (array_key_exists($Field, $Row)) {
                  // The column has an exact match in the export.
                  $Value = $Row[$Field];
               } elseif (array_key_exists($Field, $Mappings)) {
                  // The column is mapped.
                  $Value = $Row[$Mappings[$Field]];
               } else {
                  $Value = NULL;
               }
               // Format the value for writing.
               if (is_null($Value)) {
                  $Value = self::NULL;
               } elseif (is_numeric($Value)) {
                  // Do nothing, formats as is.
               } elseif (is_string($Value)) {

                  // Check to see if there is a callback filter.
                  if (isset($Filters[$Field])) {
                     $Value = call_user_func($Filters[$Field], $Value, $Field, $Row);
                  } else {
                     if($Mb && mb_detect_encoding($Value) != 'UTF-8')
                        $Value = utf8_encode($Value);
                  }

                  $Value = str_replace(array("\r\n", "\r"), array(self::NEWLINE, self::NEWLINE), $Value);
                  $Value = self::QUOTE
                     .str_replace($EscapeSearch, $EscapeReplace, $Value)
                     .self::QUOTE;
               } elseif (is_bool($Value)) {
                  $Value = $Value ? 1 : 0;
               } else {
                  // Unknown format.
                  $Value = self::NULL;
               }

               $ExRow[] = $Value;
            }
            // Write the data.
            fwrite($fp, implode(self::DELIM, $ExRow));
            // End the record.
            fwrite($fp, self::NEWLINE);
         }
      }
      if($Data !== FALSE)
         mysql_free_result($Data);
      unset($Data);

      // Write an empty line to signify the end of the table.
      fwrite($fp, self::NEWLINE);
      mysql_close();

      return $RowCount;
   }

   static function FormatElapsed($Start, $End = NULL) {
      if($End === NULL)
         $Elapsed = $Start;
      else
         $Elapsed = $End - $Start;

      $m = floor($Elapsed / 60);
      $s = $Elapsed - $m * 60;
      $Result = sprintf('%02d:%05.2f', $m, $s);

      return $Result;
   }

   public function GetCharacterSet($Table) {
      // First get the collation for the database.
      $Data = $this->Query("show table status like ':_{$Table}';");
      if (!$Data)
         return FALSE;
      if ($StatusRow = mysql_fetch_assoc($Data))
         $Collation = $StatusRow['Collation'];
      else
         return FALSE;

      // Grab the character set from the database.
      $Data = $this->Query("show collation like '$Collation'");
      if (!$Data)
         return $False;
      if ($CollationRow = mysql_fetch_assoc($Data)) {
         $CharacterSet = $CollationRow['Charset'];
         return $CharacterSet;
      }
      return FALSE;
   }

   public function GetDatabasePrefixes() {
      // Grab all of the tables.
      $Data = $this->Query('show tables');
      if ($Data === FALSE)
         return array();

      // Get the names in an array for easier parsing.
      $Tables = array();
      while (($Row = mysql_fetch_array($Data, MYSQL_NUM)) !== FALSE) {
         $Tables[] = $Row[0];
      }
      sort($Tables);

      $Prefixes = array();

      // Loop through each table and get it's prefixes.
      foreach ($Tables as $Table) {
         $PxFound = FALSE;
         foreach ($Prefixes as $PxIndex => $Px) {
            $NewPx = $this->_GetPrefix($Table, $Px);
            if (strlen($NewPx) > 0) {
               $PxFound = TRUE;
               if ($NewPx != $Px) {
                  $Prefixes[$PxIndex] = $NewPx;
               }
               break;
            }
         }
         if (!$PxFound) {
            $Prefixes[] = $Table;
         }
      }
      return $Prefixes;
   }

   protected function _GetPrefix($A, $B) {
      $Length = min(strlen($A), strlen($B));
      $Prefix = '';

      for ($i = 0; $i < $Length; $i++) {
         if ($A[$i] == $B[$i])
            $Prefix .= $A[$i];
         else
            break;
      }
      return $Prefix;
   }

   public function GetExportStructure($Row, $Structure, &$Mappings) {
      $ExportStructure = array();
      // See what columns from the structure are in

      // See what columns to add to the end of the structure.
      foreach($Row as $Column => $X) {
         if(array_key_exists($Column, $Mappings)) {
            $Mapping = $Mappings[$Column];
            if(is_string($Mapping)) {
               if(array_key_exists($Mapping, $Structure)) {
                  // This an existing column.
                  $DestColumn = $Mapping;
                  $DestType = $Structure[$DestColumn];
               } else {
                  // This is a created column.
                  $DestColumn = $Column;
                  $DestType = $Mapping;
               }
            } elseif(is_array($Mapping)) {
               $DestColumn = $Mapping['Column'];
               if (isset($Mapping['Type']))
                  $DestType = $Mapping['Type'];
               elseif(isset($Structure[$DestColumn]))
                  $DestType = $Structure[$DestColumn];
               else
                  $DestType = 'varchar(255)';
               $Mappings[$Column] = $DestColumn;
            }
         } elseif(array_key_exists($Column, $Structure)) {
            $DestColumn = $Column;
            $DestType = $Structure[$Column];
         } else {
            $DestColumn = '';
            $DestType = '';
         }

         // Check to see if we have to add the column to the export structure.
         if($DestColumn && !array_key_exists($DestColumn, $ExportStructure)) {
            // TODO: Make sure $DestType is a valid MySQL type.
            $ExportStructure[$DestColumn] = $DestType;
         }
      }
      return $ExportStructure;
   }

   protected function _GetTableHeader($Structure, $GlobalStructure) {
      $TableHeader = '';

      foreach($Structure as $Column => $Type) {
         if(strlen($TableHeader) > 0)
            $TableHeader .= self::DELIM;
         if(array_key_exists($Column, $GlobalStructure)) {
            $TableHeader .= $Column;
         } else {
            $TableHeader .= $Column.':'.$Type;
         }
      }
      return $TableHeader;
   }
   
   /**
    * Decode the HTML out of a value.
    */
   public function HTMLDecoder($Value) {
      return html_entity_decode($Value, ENT_COMPAT, 'UTF-8');
   }

    /**
    * vBulletin needs some fields decoded and it won't hurt the others.
    */
//   public function HTMLDecoder($Table, $Field, $Value) {
//      if(($Table == 'Category' || $Table == 'Discussion') && $Field == 'Name')
//         return html_entity_decode($Value);
//      else
//         return $Value;
//   }


   protected function _OpenFile() {
      if($this->UseStreaming) {
         /** Setup the output to stream the file. */

         // required for IE, otherwise Content-Disposition may be ignored
         if(ini_get('zlib.output_compression'))
            ini_set('zlib.output_compression', 'Off');

         @ob_end_clean();

         
         $fp = fopen('php://output', 'ab');
         header("Content-Disposition: attachment; filename=\"{$this->Path}\"");
         header('Content-Type: text/plain');
         header("Content-Transfer-Encoding: binary");
         header('Accept-Ranges: bytes');
         header("Cache-control: private");
         header('Pragma: private');
         header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
      } else {
         $this->Path = str_replace(' ', '_', $this->Path);
         if($this->UseCompression())
            $fp = gzopen($this->Path, 'wb');
         else
            $fp = fopen($this->Path, 'wb');
      }
      $this->_File = $fp;
      return $fp;
   }

   /** Execute a SQL query on the current connection.
    *
    * @param string $Query The sql to execute.
    * @return resource The query cursor.
    */
   public function Query($Query, $Buffer = FALSE) {
      if (isset($this->_LastResult) && is_resource($this->_LastResult))
         mysql_free_result($this->_LastResult);
      $Query = str_replace(':_', $this->Prefix, $Query); // replace prefix.

      $Connection = mysql_connect($this->_Host, $this->_Username, $this->_Password);
      mysql_select_db($this->_DbName);
      mysql_query("set names {$this->CharacterSet}");
      if ($Buffer)
         $Result = mysql_query($Query, $Connection);
      else {
         $Result = mysql_unbuffered_query($Query, $Connection);
         if (is_resource($Result))
            $this->_LastResult = $Result;
      }

      if ($Result === FALSE) {
         trigger_error(mysql_error($Connection));
      }
      
      return $Result;
   }
   
   public function SetConnection($Host = NULL, $Username = NULL, $Password = NULL, $DbName = NULL) {
      $this->_Host = $Host;
      $this->_Username = $Username;
      $this->_Password = $Password;
      $this->_DbName = $DbName;
   }

   /**
    * Returns an array of all the expected export tables and expected columns in the exports.
    * When exporting tables using ExportTable() all of the columns in this structure will always be exported in the order here, regardless of how their order in the query.
    * @return array
    * @see vnExport::ExportTable()
    */
   public function Structures() {
      return $this->_Structures;
   }

   /**
    * Whether or not to use compression on the output file.
    * @param bool $Value The value to set or NULL to just return the value.
    * @return bool
    */
   public function UseCompression($Value = NULL) {
      if($Value !== NULL)
         $this->_UseCompression = $Value;

      return $this->_UseCompression && !$this->UseStreaming && function_exists('gzopen');
   }

   /**
    * Returns the version of export file that will be created with this export.
    * The version is used when importing to determine the format of this file.
    * @return string
    */
   public function Version() {
      return VERSION;
   }

   /**
    * Checks whether or not a table and columns exist in the database.
    *
    * @param string $Table The name of the table to check.
    * @param array $Columns An array of column names to check.
    * @return bool|array The method will return one of the following
    *  - true: If table and all of the columns exist.
    *  - false: If the table does not exist.
    *  - array: The names of the missing columns if one or more columns don't exist.
    */
   public function Exists($Table, $Columns = array()) {
      $Desc = $this->Query('describe :_'.$Table);
      if ($Desc === false)
         return false;

      if (count($Columns) == 0)
         return true;
      
      $Cols = array();
      $Missing = array();
      while (($TD = mysql_fetch_assoc($Desc)) !== false) {
         $Cols[] = $TD['Field'];
      }
      foreach ($Columns as $Column) {
         if (!in_array($Column, $Cols))
            $Missing[] = $Column;
      }
      mysql_free_result($Desc);
      return count($Missing) == 0 ? true : $Missing;
   }

   /**
    * Checks all required source tables are present
    */
   public function VerifySource($RequiredTables) {
      $MissingTables = false;
      $CountMissingTables = 0;
      $MissingColumns = array();

      foreach($RequiredTables as $ReqTable => $ReqColumns) {
         $TableDescriptions = $this->Query('describe :_'.$ReqTable);
         //echo 'describe '.$Prefix.$ReqTable;
         if($TableDescriptions === false) { // Table doesn't exist
            $CountMissingTables++;
            if($MissingTables !== false)
               $MissingTables .= ', '.$ReqTable;
            else
               $MissingTables = $ReqTable;
         }
         else {
            // Build array of columns in this table
            $PresentColumns = array();
            while (($TD = mysql_fetch_assoc($TableDescriptions)) !== false) {
               $PresentColumns[] = $TD['Field'];
            }
            // Compare with required columns
            foreach($ReqColumns as $ReqCol) {
               if(!in_array($ReqCol, $PresentColumns))
                  $MissingColumns[$ReqTable][] = $ReqCol;
            }

            mysql_free_result($TableDescriptions);
         }
      }

      // Return results
      if($MissingTables === false) {
         if(count($MissingColumns) > 0) {
            $Result = array();

            // Build a string of missing columns.
            foreach ($MissingColumns as $Table => $Columns) {
               $Result[] = "The $Table table is missing the following column(s): ".implode(', ', $Columns);
            }
            return implode("<br />\n", $Result);
         }
         else return true; // Nothing missing!
      }
      elseif($CountMissingTables == count($RequiredTables)) {
         $Result = 'The required tables are not present in the database. Make sure you entered the correct database name and prefix and try again.';

         // Guess the prefixes to notify the user.
         $Prefixes = $this->GetDatabasePrefixes();
         if (count($Prefixes) == 1)
            $Result .= ' Based on the database you provided, your database prefix is probably '.implode(', ', $Prefixes);
         elseif (count($Prefixes) > 0)
            $Result .= ' Based on the database you provided, your database prefix is probably one of the following: '.implode(', ', $Prefixes);

         return $Result;
      }
      else {
         return 'Missing required database tables: '.$MissingTables;
      }
   }
}
?><?php


/* Contents included from views.php */
?><?php
/**
 * Views for Vanilla 2 export tools
 *
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */
 
   
/**
 * HTML header
 */
function PageHeader() {
   echo '<?xml version="1.0" encoding="UTF-8"?>';
      ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
   <title>Vanilla Porter - Forum Export Tool</title>
   <!-- Contents included from style.css -->
<style>
body {
	font-family: 'lucida grande','Lucida Sans Unicode', tahoma, sans-serif;
   background: url('http://vanillaforums.com/porter/slicesplash.jpg') top center no-repeat #C7E6FB;
   margin: 0px;
   padding: 0px;
   text-align: center;
   color:#076C8E;
   text-shadow:0 1px 0 #FFFFFF;   
   }
a,
a:link,
a:active,
a:visited {
   color: #2786C2;
   text-decoration: none;
   }
a:hover {
   color: #FF0084 !important;
   text-decoration: underline;
   }
div.Title {
   background:#E2F4FF none repeat scroll 0 0;
   border-top: 1px solid #A5D0E7;
   border-bottom: 1px solid #A5D0E7;
	margin: 50px 0;
	padding: 30px 0 4px;
}
div.Title h1 {
	text-align: left;
	width: 600px;
	margin: 0 auto;
}
div.Title img {
	top: 20px;
	position: absolute;
}
div.Title p {
	padding: 0 0 0 270px;
	margin: 0;
	font-size: 30px;
}

h1 {
   font-family: Arial, Helvetica, Verdana;
   color: #02455B;
   width: 568px;
   margin: 0 auto;
   padding: 0;
   font-size: 180%;
}
div.Form {
   text-align: center;
}
div.Form ul {
   width: 500px;
   margin: 0 auto;
	padding: 0;
}

div.Errors {
   background: #d00;
   padding: 20px 8px !important;
   margin: 0 0 10px;
   border-bottom: 1px solid #C0E7F5;
}
.Errors li {
   padding: 4px 0 !important;
   border: 0px !important;
   margin: 0px !important;
   color: #fff !important;
   font-size: 16px;
   line-height: 150%;
   text-shadow: #900 0 1px 0;
}
.Errors li pre,
.Errors li code {
   border-radius: 3px;
	-moz-border-radius: 3px;
	-webkit-border-radius: 3px;
	border: 1px solid #b00;
	background: #c00;
	margin: 10px 0 0;
	padding: 4px 8px;
	display: block;
	text-shadow: none;
	font-size: 13px;
	font-weight: normal;
	font-family: monospace;
}
.Errors li a {
   color: #ffff00;
	text-decoration: underline;
}
.Errors li a:hover {
   color: #ff0 !important;
	text-decoration: none;
}
.Hidden {
   display: none;
}
/* Forms */
form {
   margin: 0 0 20px;
   text-align: right;
}
form ul {
   text-align: left;
   list-style: none;
   margin: 0px;
   padding: 10px;
}
form ul li {
   padding: 10px 0;
   font-size: 18px;
}
form ul li.Warning {
   padding-bottom: 0;
   border-bottom: 0;
   font-size: 17px;
}
form ul li.Warning div {
   font-size: 14px;
	line-height: 1.6;
	color: #000;
	text-shadow: none;
   padding: 16px 0 8px;
}
form label {
   font-family: Arial, Helvetica, Verdana;
   font-weight: bold;
   display: block;
   padding: 8px 0 0;
   font-size: 110%;
	color: #02455B;
}
form label span {
	font-size: 13px;
	color: #555;
	font-weight: normal;
	text-shadow: none;
	padding: 0 0 0 10px;
}
form select {
   border-radius: 4px;
   -moz-border-radius: 4px;
   -webkit-border-radius: 4px;
   font-size: 110%;
   padding: 8px;
   width: 496px;
   border: 1px solid #ccc;
   color: #555;
}
form input.InputBox {
   border-radius: 4px;
   -moz-border-radius: 4px;
   -webkit-border-radius: 4px;
   font-size: 110%;
   padding: 8px;
   width: 480px;
   border: 1px solid #ccc;
   color: #555;
}
form input.InputBox:focus {
   color: #000;
   background: #FFFEDE;
   border: 1px solid #aaa;
}
form li.Last {
   padding: 12px 0 2px;
   border-bottom: 0;
}
div.Button {
   text-align: right;
   padding: 12px 0 30px;
   width: 496px;
   margin: 0 auto;
}
div.Button a,
input.Button {
   cursor: pointer;
   font-family: arial, helvetica, verdana;
   font-size: 25px;
   font-weight: bold;
   color: #02475A;
	text-shadow: 0 1px 0 #fff;
   margin: 0;
   padding: 3px 10px;
   background: url('http://vanillaforums.com/porter/buttonbg.png') repeat-x center left #f8f8f8;
   border: 1px solid #999;
   border-radius: 3px;
   -moz-border-radius: 3px;
   -webkit-border-radius: 3px;
	box-shadow: 0px 0px 2px #999;
	-moz-box-shadow: 0px 0px 2px #999;
	-webkit-box-shadow: 0px 0px 2px #999;  
}
div.Button a {
   padding: 4px 8px;
}
div.Button a:hover,
input.Button:hover {
   text-decoration: none;
   color: #111;
   border: 1px solid #666;
}
div.Button a:focus,
input.Button:focus {
   background: #eee;
}
/* readme.html */
div.Info {
	text-align: left;
	width: 568px;
	margin: 0 auto 0px;
	font-size: 80%;
	line-height: 1.6;
}
div.Info h1 {
	padding: 6px 0 0;
	margin: 0;
}
div.Info p {
	color: #000;
	padding: 3px 0 6px;
	margin: 0;
	text-shadow: none;
}
div.Info li {
	color: #000;
	padding: 1px 0;
	margin: 0;
	text-shadow: none;
}
.Version {
   font-size: 9pt;
   font-weight: normal;
}
</style>
</head>
<body>
<div id="Frame">
	<div id="Content">
      <div class="Title">
         <h1>
            <img src="http://vanillaforums.com/porter/vanilla_logo.png" alt="Vanilla" />
            <p>Vanilla Porter <span class="Version">Version <?php echo VERSION; ?></span></p>
         </h1>
      </div>
   <?php
}

   
/**
 * HTML footer
 */
function PageFooter() {
   ?>
   </div>
</div>
</body>
</html><?php

}

   
/**
 * Message: Write permission fail
 */
function ViewNoPermission($msg) {
   PageHeader(); ?>
   <div class="Messages Errors">
      <ul>
         <li><?php echo $msg; ?></li>
      </ul>
   </div>
   
   <?php PageFooter();
}

   
/**
 * Form: Database connection info
 */
function ViewForm($Data) {
   $forums = GetValue('Supported', $Data, array());
   $msg = GetValue('Msg', $Data, '');
   $Info = GetValue('Info', $Data, '');
   $CanWrite = GetValue('CanWrite', $Data, NULL);
   if($CanWrite === NULL)
      $CanWrite = TestWrite();

   PageHeader(); ?>
   <div class="Info">
      Welcome to the Vanilla Porter.
      This application will export your existing forum data to the Vanilla 2 import format.
      If you want more information on how to use this application go to
      <a href="http://vanillaforums.com/blog/help-topics/importing-data">http://vanillaforums.com/blog/help-topics/importing-data</a>.
   </div>
   <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
      <input type="hidden" name="step" value="info" />
      <div class="Form">
         <?php if($msg!='') : ?>
         <div class="Messages Errors">
            <ul>
               <li><?php echo $msg; ?></li>
            </ul>
         </div>
         <?php endif; ?>
         <ul>
            <li>
               <label>Source Forum Type</label>
               <select name="type">
               <?php foreach($forums as $forumClass => $forumInfo) : ?>
                  <option value="<?php echo $forumClass; ?>"<?php 
                     if(GetValue('type') == $forumClass) echo ' selected="selected"'; ?>><?php echo $forumInfo['name']; ?></option>
               <?php endforeach; ?>
               </select>
            </li>
            <li>
               <label>Table Prefix <span>Most installations have a database prefix, but if you're sure you don't have one you can leave this blank.</span></label>
               <input class="InputBox" type="text" name="prefix" value="<?php echo urlencode(GetValue('prefix')) ?>" />
            </li>
            <li>
               <label>Database Host <span>Database host is usually "localhost"</span></label>
               <input class="InputBox" type="text" name="dbhost" value="<?php echo urlencode(GetValue('dbhost', '', 'localhost')) ?>" />
            </li>
            <li>
               <label>Database Name</label>
               <input class="InputBox" type="text" name="dbname" value="<?php echo urlencode(GetValue('dbname')) ?>" />
            </li>
            <li>
               <label>Database Username</label>
               <input class="InputBox" type="text" name="dbuser" value="<?php echo urlencode(GetValue('dbuser')) ?>" />
            </li>
            <li>
               <label>Database Password</label>
               <input class="InputBox" type="password" name="dbpass" value="<?php echo GetValue('dbpass') ?>" />
            </li>
            <?php if($CanWrite): ?>
            <li>
               <label>
                  <input class="CheckBox" type="checkbox" id="savefile" name="savefile" value="savefile" <?php if(GetValue('savefile')) echo 'checked="checked"'; ?> />
                  Save the export file to the server
               </label>
            </li>
            <?php endif; ?>
         </ul>
         <div class="Button">
            <input class="Button" type="submit" value="Begin Export" />
         </div>
      </div>
   </form>
   <script type="text/javascript">
   //<![CDATA[
      function updatePrefix() {
         var type = document.getElementById('forumType').value;
         switch(type) {
            <?php foreach($forums as $forumClass => $forumInfo) : ?>
            case '<?php echo $forumClass; ?>': document.getElementById('forumPrefix').value = '<?php echo $forumInfo['prefix']; ?>'; break;
            <?php endforeach; ?>
         }
      }
   //]]>
   </script> 

   <?php PageFooter();
}


/**
 * Message: Result of export
 */
function ViewExportResult($Msgs = '', $Class = 'Info', $Path = '') {
   PageHeader();
   if($Msgs) {
      // TODO: Style this a bit better.
      echo "<div class=\"$Class\">";
      foreach($Msgs as $Msg) {
         echo "<p>$Msg</p>\n";
      }
      echo "</div>";
      if($Path)
         echo "<a href=\"$Path\"><b>Download $Path</b></a>";
   }
   PageFooter();
}

function GetValue($Key, $Collection = NULL, $Default = '') {
   if(!$Collection)
      $Collection = $_POST;
   if(array_key_exists($Key, $Collection))
      return $Collection[$Key];
   return $Default;
}
?><?php


/* Contents included from class.exportcontroller.php */
?><?php
/**
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */

/** Generic controller implemented by forum-specific ones */
abstract class ExportController {

   /** @var array Database connection info */
   protected $DbInfo = array();

   /** @var array Required tables, columns set per exporter */
   protected $SourceTables = array();

   protected $UseStreaming = FALSE;

   /** Forum-specific export routine */
   abstract protected function ForumExport($Ex);

   /**
    * Construct and set the controller's properties from the posted form.
    */
   public function __construct() {
      $this->HandleInfoForm();
   }

   /**
    * Logic for export process
    */
   public function DoExport() {
      global $Supported;

      // Test connection
      $Msg = $this->TestDatabase();
      if($Msg === true) {
         // Create db object
         $Ex = new ExportModel;
         $Ex->SetConnection($this->DbInfo['dbhost'], $this->DbInfo['dbuser'], $this->DbInfo['dbpass'], $this->DbInfo['dbname']);
         $Ex->Prefix = $this->DbInfo['prefix'];
         $Ex->UseStreaming = $this->UseStreaming;
         // Test src tables' existence structure
         $Msg = $Ex->VerifySource($this->SourceTables);
         if($Msg === true) {
            // Good src tables - Start dump
            $Ex->UseCompression(TRUE);
            $Ex->FilenamePrefix = $this->DbInfo['dbname'];
            set_time_limit(60*60);
            $this->ForumExport($Ex);

            // Write the results.
            if($Ex->UseStreaming)
               exit;
            else
               ViewExportResult($Ex->Comments, 'Info', $Ex->Path);
         }
         else
            ViewForm(array('Supported' => $Supported, 'Msg' => $Msg, 'Info' => $this->DbInfo)); // Back to form with error
      }
      else
         ViewForm(array('Supported' => $Supported, 'Msg' => $Msg, 'Info' => $this->DbInfo)); // Back to form with error
   }

   /**
    * User submitted db connection info
    */
   public function HandleInfoForm() {
      $this->DbInfo = array(
         'dbhost' => 'localhost',
         'dbuser' => 'root',
         'dbpass' => 'Id0nTkN0w',
         'dbname' => 'rock',
         'type'   => 'SMF',
         'prefix' => 'smf_');
      $this->UseStreaming = FALSE;
   }

   /**
    * Test database connection info
    */
   public function TestDatabase() {
      // Connection
      if($C = @mysql_connect($this->DbInfo['dbhost'], $this->DbInfo['dbuser'], $this->DbInfo['dbpass'])) {
         // Database
         if(mysql_select_db($this->DbInfo['dbname'], $C)) {
            mysql_close($C);
            return true;
         }
         else {
            mysql_close($C);
            return 'Could not find database &ldquo;'.$this->DbInfo['dbname'].'&rdquo;.';
         }
      }
      else
         return 'Could not connect to '.$this->DbInfo['dbhost'].' as '.$this->DbInfo['dbuser'].' with given password.';
   }
}
?><?php



/* Contents included from class.vanilla1.php */
?><?php
/**
 * Vanilla 1 exporter tool
 *
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */
 
class Vanilla1 extends ExportController {

   /** @var array Required tables => columns */  
   public $SourceTables = array(
      'User'=> array('UserID', 'Name', 'Password', 'Email', 'CountComments'),
      'Role'=> array('RoleID', 'Name', 'Description'),
      'Category'=> array('CategoryID', 'Name', 'Description'),
      'Discussion'=> array('DiscussionID', 'Name', 'CategoryID', 'DateCreated', 'AuthUserID', 'DateLastActive', 'Closed', 'Sticky', 'CountComments', 'Sink', 'LastUserID'),
      'Comment'=> array('CommentID', 'DiscussionID', 'AuthUserID', 'DateCreated', 'EditUserID', 'DateEdited', 'Body')
      );
   
   /**
    * Forum-specific export format
    * @todo Project file size / export time and possibly break into multiple files
    * 
    */
   protected function ForumExport($Ex) {
      // Get the characterset for the comments.
      $CharacterSet = $Ex->GetCharacterSet('Comment');
      if ($CharacterSet)
         $Ex->CharacterSet = $CharacterSet;

      // Begin
      $Ex->BeginExport('', 'Vanilla 1.*');
      
      // Users
      $User_Map = array(
         'UserID'=>'UserID',
         'Name'=>'Name',
         'Password'=>'Password',
         'Email'=>'Email',
         'Icon'=>'Photo',
         'CountComments'=>'CountComments'
      );   
      $Ex->ExportTable('User', "SELECT * FROM :_User", $User_Map);  // ":_" will be replaced by database prefix
      
      // Roles
      /*
		    'RoleID' => 'int', 
		    'Name' => 'varchar(100)', 
		    'Description' => 'varchar(200)'
		 */
      $Role_Map = array(
         'RoleID'=>'RoleID',
         'Name'=>'Name',
         'Description'=>'Description'
      );   
      $Ex->ExportTable('Role', 'select * from :_Role', $Role_Map);
  
      // UserRoles
      /*
		    'UserID' => 'int', 
		    'RoleID' => 'int'
		 */
      $UserRole_Map = array(
         'UserID' => 'UserID', 
         'RoleID'=> 'RoleID'
      );
      $Ex->ExportTable('UserRole', 'select UserID, RoleID from :_User', $UserRole_Map);
      
      // Categories
      /*
          'CategoryID' => 'int', 
          'Name' => 'varchar(30)', 
          'Description' => 'varchar(250)', 
          'ParentCategoryID' => 'int', 
          'DateInserted' => 'datetime', 
          'InsertUserID' => 'int', 
          'DateUpdated' => 'datetime', 
          'UpdateUserID' => 'int'
		 */
      $Category_Map = array(
         'CategoryID' => 'CategoryID', 
         'Name' => 'Name',
         'Description'=> 'Description'
      );
      $Ex->ExportTable('Category', "select CategoryID, Name, Description from :_Category", $Category_Map);
      
      // Discussions
      /*
		    'DiscussionID' => 'int', 
		    'Name' => 'varchar(100)', 
		    'CategoryID' => 'int', 
		    'Body' => 'text', 
		    'Format' => 'varchar(20)', 
		    'DateInserted' => 'datetime', 
		    'InsertUserID' => 'int', 
		    'DateUpdated' => 'datetime', 
		    'UpdateUserID' => 'int', 
		    'Score' => 'float', 
		    'Announce' => 'tinyint', 
		    'Closed' => 'tinyint'
		 */
      $Discussion_Map = array(
         'DiscussionID' => 'DiscussionID', 
         'Name' => 'Name',
         'CategoryID'=> 'CategoryID',
         'DateCreated'=>'DateInserted',
         'DateCreated2'=>'DateUpdated',
         'AuthUserID'=>'InsertUserID',
         'DateLastActive'=>'DateLastComment',
         'AuthUserID2'=>'UpdateUserID',
         'Closed'=>'Closed',
         'Sticky'=>'Announce',
         'CountComments'=>'CountComments',
         'Sink'=>'Sink',
         'LastUserID'=>'LastCommentUserID'
      );
      $Ex->ExportTable('Discussion',
         "SELECT d.*,
            d.LastUserID as LastCommentUserID,
            d.DateCreated as DateCreated2, d.AuthUserID as AuthUserID2
         FROM :_Discussion d
         WHERE coalesce(d.WhisperUserID, 0) = 0", $Discussion_Map);
      
      // Comments
      /*
		    'CommentID' => 'int', 
		    'DiscussionID' => 'int', 
		    'DateInserted' => 'datetime', 
		    'InsertUserID' => 'int', 
		    'DateUpdated' => 'datetime', 
		    'UpdateUserID' => 'int', 
		    'Format' => 'varchar(20)', 
		    'Body' => 'text', 
		    'Score' => 'float'
		 */
      $Comment_Map = array(
         'CommentID' => 'CommentID',
         'DiscussionID' => 'DiscussionID',
         'AuthUserID' => 'InsertUserID',
         'DateCreated' => 'DateInserted',
         'EditUserID' => 'UpdateUserID',
         'DateEdited' => 'DateUpdated',
         'Body' => 'Body',
         'FormatType' => 'Format'
      );
      $Ex->ExportTable('Comment', "
         SELECT 
            c.*
         FROM :_Comment c
         JOIN :_Discussion d
            ON c.DiscussionID = d.DiscussionID
         WHERE coalesce(d.WhisperUserID, 0) = 0
            AND coalesce(c.WhisperUserID, 0) = 0", $Comment_Map);

      $Ex->ExportTable('UserDiscussion', "
         SELECT
            w.UserID,
            w.DiscussionID,
            w.CountComments,
            w.LastViewed as DateLastViewed,
            case when b.UserID is not null then 1 else 0 end AS Bookmarked
         FROM :_UserDiscussionWatch w
         LEFT JOIN :_UserBookmark b
            ON w.DiscussionID = b.DiscussionID AND w.UserID = b.UserID");
      
      // Conversations

      // Create a mapping table for conversations.
      // This cannot be a temporary table because of some of the union selects it is used in below.
      $Ex->Query("create table LUM_V1Conversation (ConversationID int auto_increment primary key, DiscussionID int, UserID1 int, UserID2 int, DateCreated datetime, EditUserID int, DateEdited datetime)");

      $Ex->Query("insert LUM_V1Conversation (DiscussionID, UserID1, UserID2, DateCreated, EditUserID, DateEdited)
         select
           DiscussionID,
           AuthUserID as UserID1,
           WhisperUserID as UserID2,
           min(DateCreated),
           max(EditUserID),
           max(DateEdited)
         from LUM_Comment
         where coalesce(WhisperUserID, 0) <> 0
         group by DiscussionID, AuthUserID, WhisperUserID

         union

         select
           DiscussionID,
           AuthUserID as UserID1,
           WhisperUserID as UserID2,
           DateCreated,
           WhisperFromLastUserID,
           DateLastWhisper
         from LUM_Discussion
         where coalesce(WhisperUserID, 0) <> 0");

      // Delete redundant conversations.
      $Ex->Query("create index ix_V1UserID1 on LUM_V1Conversation (DiscussionID, UserID1)"); // for speed
      $Ex->Query("delete t.*
         from LUM_V1Conversation t
         inner join LUM_Comment c
           on c.DiscussionID = t.DiscussionID
             and c.AuthUserID = t.UserID2
             and c.WhisperUserID = t.UserID1
             and c.AuthUserID < c.WhisperUserID");


      $Conversation_Map = array(
         'UserID1' => 'InsertUserID',
         'DateCreated' => 'DateInserted',
         'EditUserID' => 'UpdateUserID',
         'DateEdited' => 'DateUpdated'
      );
      $Ex->ExportTable('Conversation', "select * from :_V1Conversation", $Conversation_Map);
      
      // ConversationMessage
      /*
         'MessageID' => 'int', 
         'ConversationID' => 'int', 
         'Body' => 'text', 
         'InsertUserID' => 'int', 
         'DateInserted' => 'datetime'
      */
      $ConversationMessage_Map = array(
         'CommentID' => 'MessageID',
         'DiscussionID' => 'ConversationID',
         'Body' => 'Body',
         'AuthUserID' => 'InsertUserID',
         'DateCreated' => 'DateInserted'
      );
      $Ex->ExportTable('ConversationMessage', "
         select c.CommentID, t.ConversationID, c.AuthUserID, c.DateCreated, c.Body
         from LUM_Comment c
         join LUM_V1Conversation t
           on t.DiscussionID = c.DiscussionID
             and c.WhisperUserID in (t.UserID1, t.UserID2)
             and c.AuthUserID in (t.UserID1, t.UserID2)
         where c.WhisperUserID > 0

         union

         select c.CommentID, t.ConversationID, c.AuthUserID, c.DateCreated, c.Body
         from LUM_Comment c
         join LUM_Discussion d
          on c.DiscussionID = d.DiscussionID
         join LUM_V1Conversation t
           on t.DiscussionID = d.DiscussionID
             and d.WhisperUserID in (t.UserID1, t.UserID2)
             and d.AuthUserID in (t.UserID1, t.UserID2)
         where d.WhisperUserID > 0", $ConversationMessage_Map);
      
      // UserConversation
      /*
         'UserID' => 'int', 
         'ConversationID' => 'int', 
         'LastMessageID' => 'int'
      */
      $UserConversation_Map = array(
         'UserID' => 'UserID',
         'ConversationID' => 'ConversationID'
      );
      $Ex->ExportTable('UserConversation', 
         "select UserID1 as UserID, ConversationID
         from LUM_V1Conversation

         union

         select UserID2 as UserID, ConversationID
         from LUM_V1Conversation", $UserConversation_Map);

      $Ex->Query("drop table :_V1Conversation");
         
      // End
      $Ex->EndExport();
   }

}
?>
<?php


/* Contents included from class.vbulletin.php */
?><?php
/**
 * vBulletin exporter tool
 *
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */
 
class Vbulletin extends ExportController {
   
   /** @var array Required tables => columns */
   protected $SourceTables = array(
      'user' => array('userid','username','password','email','referrerid','timezoneoffset','posts','salt',
         'birthday_search','joindate','lastvisit','lastactivity','membergroupids','usergroupid',
         'usertitle', 'homepage', 'aim', 'icq', 'yahoo', 'msn', 'skype', 'styleid', 'avatarid'),
      'usergroup'=> array('usergroupid','title','description'),
      'userfield' => array('userid'),
      'phrase' => array('varname','text','product','fieldname','varname'),
      'thread' => array('threadid','forumid','postuserid','title','open','sticky','dateline','lastpost'),
      'deletionlog' => array('type','primaryid'),
      'post' => array('postid','threadid','pagetext','userid','dateline'),
      'forum' => array('forumid','description','displayorder','title','description','displayorder'),
      'subscribethread' => array('userid','threadid')
   );
   
   /**
    * Forum-specific export format
    * @todo Project file size / export time and possibly break into multiple files
    */
   protected function ForumExport($Ex) {
      // Begin
      $Ex->BeginExport('', 'vBulletin 3.*');
      
      // Users
      $User_Map = array(
         'userid'=>'UserID',
         'username'=>'Name',
         'password2'=>'Password',
         'email'=>'Email',
         'referrerid'=>'InviteUserID',
         'timezoneoffset'=>'HourOffset',
         //'posts'=>'CountComments',
         'salt'=>'char(3)',
         'photopath'=>'Photo'
      );
      $Ex->ExportTable('User', "select *,
				concat(`password`, salt) as password2,
				concat('avatar', userid, '_', avatarid, '.gif') as photopath,
            DATE_FORMAT(birthday_search,GET_FORMAT(DATE,'ISO')) as DateOfBirth,
            FROM_UNIXTIME(joindate) as DateFirstVisit,
            FROM_UNIXTIME(lastvisit) as DateLastActive,
            FROM_UNIXTIME(joindate) as DateInserted,
            FROM_UNIXTIME(lastactivity) as DateUpdated
         from :_user", $User_Map);  // ":_" will be replace by database prefix
      
      // Roles
      $Role_Map = array(
         'usergroupid'=>'RoleID',
         'title'=>'Name',
         'description'=>'Description'
      );   
      $Ex->ExportTable('Role', 'select * from :_usergroup', $Role_Map);
    
      // UserRoles
      $UserRole_Map = array(
         'userid'=>'UserID',
         'usergroupid'=>'RoleID'
      );
      $Ex->Query("CREATE TEMPORARY TABLE VbulletinRoles (userid INT UNSIGNED NOT NULL, usergroupid INT UNSIGNED NOT NULL)");
      # Put primary groups into tmp table
      $Ex->Query("insert into VbulletinRoles (userid, usergroupid) select userid, usergroupid from :_user");
      # Put stupid CSV column into tmp table
      $SecondaryRoles = $Ex->Query("select userid, usergroupid, membergroupids from :_user", TRUE);
      if (is_resource($SecondaryRoles)) {
         while (($Row = @mysql_fetch_assoc($SecondaryRoles)) !== false) {
            if($Row['membergroupids']!='') {
               $Groups = explode(',',$Row['membergroupids']);
               foreach($Groups as $GroupID) {
                  $Ex->Query("insert into VbulletinRoles (userid, usergroupid) values({$Row['userid']},{$GroupID})", TRUE);
               }
            }
         }
      }
      # Export from our tmp table and drop
      $Ex->ExportTable('UserRole', 'select distinct userid, usergroupid from VbulletinRoles', $UserRole_Map);
      $Ex->Query("DROP TABLE IF EXISTS VbulletinRoles");
      
      // UserMeta
      $Ex->Query("CREATE TEMPORARY TABLE VbulletinUserMeta (`UserID` INT NOT NULL ,`MetaKey` VARCHAR( 64 ) NOT NULL ,`MetaValue` VARCHAR( 255 ) NOT NULL)");
      # Standard vB user data
      $UserFields = array('usertitle', 'homepage', 'aim', 'icq', 'yahoo', 'msn', 'skype', 'styleid');
      foreach($UserFields as $Field)
         $Ex->Query("insert into VbulletinUserMeta (UserID, MetaKey, MetaValue) select userid, '$Field.', $Field from :_user where $Field !=''");
      # Dynamic vB user data (userfield)
      $ProfileFields = $Ex->Query("select varname, text from :_phrase where product='vbulletin' and fieldname='cprofilefield' and varname like 'field%_title'");
      if (is_resource($ProfileFields)) {
         while (($Field = @mysql_fetch_assoc($ProfileFields)) !== false) {
         //foreach ($ProfileFields as $Field) {
            $VbulletinField = str_replace('_title','',$Field['varname']);
            $MetaKey = preg_replace('/[^0-9a-z_-]/','',strtolower($Field['text']));
            $Ex->Query("insert into VbulletinUserMeta (UserID, MetaKey, MetaValue)
               select userid, '".$MetaKey."', ".$VbulletinField." from :_userfield where ".$VbulletinField."!=''");
         }
      }
      # Export from our tmp table and drop
      $Ex->ExportTable('UserMeta', 'select UserID, MetaKey as Name, MetaValue as Value from VbulletinUserMeta');
      $Ex->Query("DROP TABLE IF EXISTS VbulletinUserMeta");
      
      // Categories
      $Category_Map = array(
         'forumid'=>'CategoryID',
         'description'=>'Description',
         'Name'=>array('Column'=>'Name','Filter'=>array($Ex, 'HTMLDecoder')),
         'displayorder'=>array('Column'=>'Sort', 'Type'=>'int')
      );
      $Ex->ExportTable('Category', "select forumid, left(title,30) as Name, description, displayorder
         from :_forum where threadcount > 0", $Category_Map);
      
      // Discussions
      $Discussion_Map = array(
         'threadid'=>'DiscussionID',
         'forumid'=>'CategoryID',
         'postuserid'=>'InsertUserID',
         'postuserid2'=>'UpdateUserID',
         'title'=>array('Column'=>'Name','Filter'=>array($Ex, 'HTMLDecoder')),
			'Format'=>'Format'
      );
      $Ex->ExportTable('Discussion', "select t.*,
				t.postuserid as postuserid2,
            p.pagetext as Body,
				'BBCode' as Format,
            replycount+1 as CountComments, 
            convert(ABS(open-1),char(1)) as Closed, 
            convert(sticky,char(1)) as Announce,
            FROM_UNIXTIME(t.dateline) as DateInserted,
            FROM_UNIXTIME(lastpost) as DateUpdated,
            FROM_UNIXTIME(lastpost) as DateLastComment
         from :_thread t
            left join :_deletionlog d ON (d.type='thread' AND d.primaryid=t.threadid)
				left join :_post p ON p.postid = t.firstpostid
         where d.primaryid IS NULL", $Discussion_Map);
      
      // Comments
      $Comment_Map = array(
         'postid' => 'CommentID',
         'threadid' => 'DiscussionID',
         'pagetext' => 'Body',
			'Format' => 'Format'
      );
      $Ex->ExportTable('Comment', "select p.*,
				'BBCode' as Format,
            p.userid as InsertUserID,
            p.userid as UpdateUserID,
            FROM_UNIXTIME(p.dateline) as DateInserted,
            FROM_UNIXTIME(p.dateline) as DateUpdated
         from :_post p
				inner join :_thread t ON p.threadid = t.threadid
            left join :_deletionlog d ON (d.type='post' AND d.primaryid=p.postid)
         where p.postid <> t.firstpostid and d.primaryid IS NULL", $Comment_Map);
      
      // UserDiscussion
		$UserDiscussion_Map = array(
			'DateLastViewed' =>  'datetime');
      $Ex->ExportTable('UserDiscussion', "select
           tr.userid as UserID,
           tr.threadid as DiscussionID,
           FROM_UNIXTIME(tr.readtime) as DateLastViewed,
           case when st.threadid is not null then 1 else 0 end as Bookmarked
         from :_threadread tr
         left join :_subscribethread st on tr.userid = st.userid and tr.threadid = st.threadid");
      
      // Activity (3.8+)
      $Activity_Map = array(
         'postuserid'=>'ActivityUserID',
         'userid'=>'RegardingUserID',
         'pagetext'=>'Story',
         'postuserid'=>'InsertUserID'
      );
		$Tables = $Ex->Query("show tables like ':_visitormessage'");
      if (mysql_fetch_assoc($Tables) !== FALSE) { # Table is present
			$Ex->ExportTable('Activity', "select *, 
			   FROM_UNIXTIME(dateline) as DateInserted
			from :_visitormessage
			where state='visible'", $Activity_Map);
      }
      
      // End
      $Ex->EndExport();
   }
   
}
?><?php


/* Contents included from class.phpbb.php */
?><?php
/**
 * phpBB exporter tool
 *
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */

class Phpbb extends ExportController {

   /** @var array Required tables => columns */
   protected $SourceTables = array(
      'users' => array('user_id', 'username', 'user_password', 'user_email', 'user_timezone', 'user_posts', 'user_regdate', 
         'user_lastvisit', 'user_regdate'),
      'groups' => array('group_id', 'group_name', 'group_desc'),
      'user_group' => array('user_id', 'group_id'),
      'forums' => array('forum_id', 'forum_name', 'forum_desc', 'left_id', 'parent_id'),
      'topics' => array('topic_id', 'forum_id', 'topic_poster',  'topic_title', 'topic_views', 'topic_first_post_id', 
         'topic_replies', 'topic_status', 'topic_type', 'topic_time', 'topic_last_post_time', 'topic_last_post_time'),
      'posts' => array('post_id', 'topic_id', 'post_text', 'poster_id', 'post_edit_user', 'post_time', 'post_edit_time'),
      'bookmarks' => array('user_id', 'topic_id')
   );

   /**
    * Forum-specific export format.
    * @param ExportModel $Ex
    */
   protected function ForumExport($Ex) {
      // Begin
      $Ex->BeginExport('', 'phpBB 3.*', array('HashMethod' => 'phpBB'));

      // Users
      $User_Map = array(
         'user_id'=>'UserID',
         'username'=>'Name',
         'user_password'=>'Password',
         'user_email'=>'Email',
         'user_timezone'=>'HourOffset',
         'user_posts'=>array('Column' => 'CountComments', 'Type' => 'int')
      );
      $Ex->ExportTable('User', "select *,
            FROM_UNIXTIME(nullif(user_regdate, 0)) as DateFirstVisit,
            FROM_UNIXTIME(nullif(user_lastvisit, 0)) as DateLastActive,
            FROM_UNIXTIME(nullif(user_regdate, 0)) as DateInserted
         from :_users", $User_Map);  // ":_" will be replace by database prefix


      // Roles
      $Role_Map = array(
         'group_id'=>'RoleID',
         'group_name'=>'Name',
         'group_desc'=>'Description'
      );
      $Ex->ExportTable('Role', 'select * from :_groups', $Role_Map);


      // UserRoles
      $UserRole_Map = array(
         'user_id'=>'UserID',
         'group_id'=>'RoleID'
      );
      $Ex->ExportTable('UserRole', 'select user_id, group_id from :_users
         union
         select user_id, group_id from :_user_group', $UserRole_Map);

      // Categories
      $Category_Map = array(
         'forum_id'=>'CategoryID',
         'forum_name'=>'Name',
         'forum_desc'=>'Description',
         'left_id'=>'Sort'
      );
      $Ex->ExportTable('Category', "select *,
         nullif(parent_id,0) as ParentCategoryID
         from :_forums", $Category_Map);


      // Discussions
      $Discussion_Map = array(
         'topic_id'=>'DiscussionID',
         'forum_id'=>'CategoryID',
         'topic_poster'=>'InsertUserID',
         'topic_title'=>'Name',
			'Format'=>'Format',
         'topic_views'=>'CountViews',
         'topic_first_post_id'=>array('Column'=>'FirstCommentID','Type'=>'int')
      );
      $Ex->ExportTable('Discussion', "select t.*,
				'BBCode' as Format,
            topic_replies+1 as CountComments,
            case t.topic_status when 1 then 1 else 0 end as Closed,
            case t.topic_type when 1 then 1 else 0 end as Announce,
            FROM_UNIXTIME(t.topic_time) as DateInserted,
            FROM_UNIXTIME(t.topic_last_post_time) as DateUpdated,
            FROM_UNIXTIME(t.topic_last_post_time) as DateLastComment
         from :_topics t", $Discussion_Map);

      // Comments
      $Comment_Map = array(
         'post_id' => 'CommentID',
         'topic_id' => 'DiscussionID',
         'post_text' => array('Column'=>'Body','Filter'=>array($this, 'RemoveBBCodeUIDs')),
			'Format' => 'Format',
         'poster_id' => 'InsertUserID',
         'post_edit_user' => 'UpdateUserID'
      );
      $Ex->ExportTable('Comment', "select p.*,
				'BBCode' as Format,
            FROM_UNIXTIME(p.post_time) as DateInserted,
            FROM_UNIXTIME(nullif(p.post_edit_time,0)) as DateUpdated
         from :_posts p", $Comment_Map);

      // UserDiscussion
		$UserDiscussion_Map = array(
			'user_id' =>  'UserID',
         'topic_id' => 'DiscussionID');
      $Ex->ExportTable('UserDiscussion', "select b.*,
         1 as Bookmarked
         from :_bookmarks b", $UserDiscussion_Map);

      // End
      $Ex->EndExport();
   }

   public function RemoveBBCodeUIDs($Value, $Field, $Row) {
      $UID = $Row['bbcode_uid'];
      return str_replace(':'.$UID, '', $Value);
   }
}
?><?php


/* Contents included from class.bbpress.php */
?><?php
/**
 * ppPress exporter tool
 *
 * @copyright Vanilla Forums Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */

class BbPress extends ExportController {

   /** @var array Required tables => columns */
   protected $SourceTables = array(
      'forums' => array(),
      'posts' => array(),
      'topics' => array(),
      'users' => array('ID', 'user_nicename', 'user_pass', 'user_email', 'user_registered'),
      'meta' => array()
   );
   
   /**
    * Forum-specific export format.
    * @param ExportModel $Ex
    */
   protected function ForumExport($Ex) {
      // Begin
      $Ex->BeginExport('', 'bbPress 1.*', array('HashMethod' => 'Vanilla'));

      // Users
      $User_Map = array(
         'ID'=>'UserID',
         'user_nicename'=>'Name',
         'user_pass'=>'Password',
         'user_email'=>'Email',
         'user_registered'=>'DateInserted'
      );
      $Ex->ExportTable('User', "select * from :_users", $User_Map);  // ":_" will be replace by database prefix

      // Roles
      $Ex->ExportTable('Role', 
         "select 1 as RoleID, 'Guest' as Name
         union select 2, 'Key Master'
         union select 3, 'Administrator'
         union select 4, 'Moderator'
         union select 5, 'Member'
         union select 6, 'Inactive'
         union select 7, 'Blocked'");


      // UserRoles
      $UserRole_Map = array(
         'user_id'=>'UserID'
      );
      $Ex->ExportTable('UserRole', 
         "select distinct
           user_id,
           case when locate('keymaster', meta_value) <> 0 then 2
           when locate('administrator', meta_value) <> 0 then 3
           when locate('moderator', meta_value) <> 0 then 4
           when locate('member', meta_value) <> 0 then 5
           when locate('inactive', meta_value) <> 0 then 6
           when locate('blocked', meta_value) <> 0 then 7
           else 1 end as RoleID
         from :_usermeta
         where meta_key = 'bb_capabilities'", $UserRole_Map);

      // Categories
      $Category_Map = array(
         'forum_id'=>'CategoryID',
         'forum_name'=>'Name',
         'forum_desc'=>'Description',
         'form_slug'=>'UrlCode',
         'left_order'=>'Sort'
      );
      $Ex->ExportTable('Category', "select *,
         nullif(forum_parent,0) as ParentCategoryID
         from :_forums", $Category_Map);


      // Discussions
      $Discussion_Map = array(
         'topic_id'=>'DiscussionID',
         'forum_id'=>'CategoryID',
         'topic_poster'=>'InsertUserID',
         'topic_title'=>'Name',
			'Format'=>'Format',
         'topic_start_time'=>'DateInserted',
         'topic_sticky'=>'Announce'
      );
      $Ex->ExportTable('Discussion', "select t.*,
				'Html' as Format,
            case t.topic_open when 0 then 1 else 0 end as Closed
         from :_topics t", $Discussion_Map);

      // Comments
      $Comment_Map = array(
         'post_id' => 'CommentID',
         'topic_id' => 'DiscussionID',
         'post_text' => 'Body',
			'Format' => 'Format',
         'Body' => array('Column'=>'Body','Filter'=>'bbPressTrim'),
         'poster_id' => 'InsertUserID',
         'post_time' => 'DateInserted'
      );
      $Ex->ExportTable('Comment', "select p.*,
				'Html' as Format
         from :_posts p", $Comment_Map);

      // Conversations.

      // The export is different depending on the table layout.
      $PM = $Ex->Exists('bbpm', array('ID', 'pm_title', 'pm_from', 'pm_to', 'pm_text', 'sent_on', 'pm_thread'));
      $ConversationVersion = '';

      if ($PM === TRUE) {
         // This is from an old version of the plugin.
         $ConversationVersion = 'old';
      } elseif (is_array($PM) && count(array_intersect(array('ID', 'pm_from', 'pm_text', 'sent_on', 'pm_thread'), $PM)) == 0) {
         // This is from a newer version of the plugin.
         $ConversationVersion = 'new';
      }

      if ($ConversationVersion) {
         // Conversation.
         $Conv_Map = array(
            'pm_thread' => 'ConversationID',
            'pm_from' => 'InsertUserID'
         );
         $Ex->ExportTable('Conversation',
            "select *, from_unixtime(sent_on) as DateInserted
            from :_bbpm
            where thread_depth = 0", $Conv_Map);

         // ConversationMessage.
         $ConvMessage_Map = array(
            'ID' => 'MessageID',
            'pm_thread' => 'ConversationID',
            'pm_from' => 'InsertUserID',
            'pm_text' => array('Column'=>'Body','Filter'=>'bbPressTrim')
         );
         $Ex->ExportTable('ConversationMessage',
            'select *, from_unixtime(sent_on) as DateInserted
            from :_bbpm', $ConvMessage_Map);

         // UserConversation.
         $Ex->Query("create temporary table bbpmto (UserID int, ConversationID int)");

         if ($ConversationVersion == 'new') {
            $To = $Ex->Query("select object_id, meta_value from bb_meta where object_type = 'bbpm_thread' and meta_key = 'to'", TRUE);
            if (is_resource($To)) {
               while (($Row = @mysql_fetch_assoc($To)) !== false) {
                  $Thread = $Row['object_id'];
                  $Tos = explode(',', trim($Row['meta_value'], ','));
                  $ToIns = '';
                  foreach ($Tos as $ToID) {
                     $ToIns .= "($ToID,$Thread),";
                  }
                  $ToIns = trim($ToIns, ',');

                  $Ex->Query("insert bbpmto (UserID, ConversationID) values $ToIns", TRUE);
               }
               mysql_free_result($To);

               $Ex->ExportTable('UserConversation', 'select * from bbpmto');
            }
         } else {
            $ConUser_Map = array(
                'pm_thread' => 'ConversationID',
                'pm_from' => 'UserID'
            );
            $Ex->ExportTable('UserConversation',
               'select distinct
                 pm_thread,
                 pm_from,
                 del_sender as Deleted
               from bb_bbpm

               union

               select distinct
                 pm_thread,
                 pm_to,
                 del_reciever
               from bb_bbpm', $ConUser_Map);
         }
      }


         

         

      // End
      $Ex->EndExport();
   }
}

function bbPressTrim($Text) {
   return rtrim(bb_code_trick_reverse($Text));
}

function bb_code_trick_reverse( $text ) {
   $text = preg_replace_callback("!(<pre><code>|<code>)(.*?)(</code></pre>|</code>)!s", 'bb_decodeit', $text);
   $text = str_replace(array('<p>', '<br />'), '', $text);
   $text = str_replace('</p>', "\n", $text);
   $text = str_replace('<coded_br />', '<br />', $text);
   $text = str_replace('<coded_p>', '<p>', $text);
   $text = str_replace('</coded_p>', '</p>', $text);
   return $text;
}

function bb_decodeit( $matches ) {
	$text = $matches[2];
	$trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
	$text = strtr($text, $trans_table);
	$text = str_replace('<br />', '<coded_br />', $text);
	$text = str_replace('<p>', '<coded_p>', $text);
	$text = str_replace('</p>', '</coded_p>', $text);
	$text = str_replace(array('&#38;','&amp;'), '&', $text);
	$text = str_replace('&#39;', "'", $text);
	if ( '<pre><code>' == $matches[1] )
		$text = "\n$text\n";
	return "`$text`";
}
?><?php


/* Written by John Crenshaw for Priacta, Inc. */
?><?php
/**
 * SMF exporter tool
 *
 * @copyright Priacta, Inc. 2010
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPL2
 * @package VanillaPorter
 */

class SMF extends ExportController {

   /** @var array Required tables => columns */
   protected $SourceTables = array(
      'boards' => array(),
      'messages' => array(),
      'instant_messages' => array(),
	  'im_recipients' => array(),
	  'categories' => array('ID_CAT', 'name', 'catOrder'),
	  'membergroups' => array(),
      'members' => array('ID_MEMBER', 'memberName', 'passwd', 'emailAddress', 'dateRegistered')
   );
   
   /**
    * Forum-specific export format.
    * @param ExportModel $Ex
    */
   protected function ForumExport($Ex) {
      // Begin
      $Ex->BeginExport('', 'SMF 1.*', array('HashMethod' => 'reset'));

      // Users
      $User_Map = array(
         'ID_MEMBER'=>'UserID',
         'memberName'=>'Name',
         'password'=>'Password', // concat(`passwd`, `passwordSalt`) as `password`
         'emailAddress'=>'Email',
         'DateInserted'=>'DateInserted',
		 'timeOffset'=>'HourOffset',
         'posts'=>'CountComments',
         'avatar'=>'Photo',
         'birthdate'=>'DateOfBirth',
         'DateFirstVisit'=>'DateFirstVisit',
         'DateLastActive'=>'DateLastActive',
         'DateUpdated'=>'DateUpdated'
      );
      $Ex->ExportTable('User', "
	  select *,
             from_unixtime(dateRegistered) as DateInserted,
			 from_unixtime(dateRegistered) as DateFirstVisit,
			 from_unixtime(lastLogin) as DateLastActive,
			 from_unixtime(lastLogin) as DateUpdated,
			 concat(`passwd`, `passwordSalt`) as `password`
	  from :_members", $User_Map);

      // Roles
      $Role_Map = array(
         'ID_GROUP'=>'RoleID',
         'groupName'=>'Name'
      );
      $Ex->ExportTable('Role', "select * from :_membergroups", $Role_Map);

      // UserRoles
      $UserRole_Map = array(
         'ID_MEMBER'=>'UserID',
		 'ID_GROUP'=>'RoleID'
      );
      $Ex->ExportTable('UserRole', "select * from :_members", $UserRole_Map);
	  
      // Categories
      $Ex->ExportTable('Category',
	  "
	  
      select
        (`ID_CAT` + 1000000) as `CategoryID`,
        `name` as `Name`,
		'' as `Description`,
		null as `ParentCategoryID`,
        `catOrder` as `Sort`
      from :_categories
	  
	  union
	  
      select
        `ID_BOARD` as `CategoryID`,
        `name` as `Name`,
		`description` as `Description`,
		(CASE WHEN `ID_PARENT` = 0 THEN (`ID_CAT` + 1000000) ELSE `ID_PARENT` END) as `ParentCategoryID`,
        `boardOrder` as `Sort`
      from :_boards
	  
	  ");
	  
      // Discussions
      $Discussion_Map = array(
         'ID_TOPIC' => 'DiscussionID', 
         'subject' => array('Column'=>'Name','Filter'=>'bb2html'),
		 'body' => array('Column'=>'Body','Filter'=>'bb2html'),
		 'Format'=>'Format',
         'ID_BOARD'=> 'CategoryID',
         'DateInserted'=>'DateInserted',
         'DateUpdated'=>'DateUpdated',
         'ID_MEMBER'=>'InsertUserID',
         'DateLastComment'=>'DateLastComment',
         'UpdateUserID'=>'UpdateUserID',
         'locked'=>'Closed',
         'isSticky'=>'Announce',
         'CountComments'=>'CountComments',
		 'numViews'=>'CountViews',
         'LastCommentUserID'=>'LastCommentUserID',
		 'ID_LAST_MSG'=>'LastCommentID'
      );
      $Ex->ExportTable('Discussion', "
	  select t.*,
	     (t.numReplies + 1) as CountComments,
	         m.subject,
			 m.body,
			 from_unixtime(m.posterTime) as DateInserted,
			 from_unixtime(m.modifiedTime) as DateUpdated,
			 m.ID_MEMBER,
			 from_unixtime(m_end.posterTime) AS DateLastComment,
			 m_end.ID_MEMBER AS UpdateUserID,
			 m_end.ID_MEMBER AS LastCommentUserID,
			 'Html' as Format
         from :_topics t
		 join :_messages as m on t.ID_FIRST_MSG = m.ID_MSG
		 join :_messages as m_end on t.ID_LAST_MSG = m_end.ID_MSG
		 
		 -- where t.spam = 0 AND m.spam = 0;
		 
		 ", $Discussion_Map);

      // Comments
      $Comment_Map = array(
         'ID_MSG' => 'CommentID',
         'ID_TOPIC' => 'DiscussionID',
		 'Format' => 'Format',
         'body' => array('Column'=>'Body','Filter'=>'bb2html'),
         'ID_MEMBER' => 'InsertUserID',
         'DateInserted' => 'DateInserted'
      );
      $Ex->ExportTable('Comment', "select m.*, from_unixtime(m.posterTime) AS DateInserted,
				'Html' as Format
         from :_messages m
		 join :_topics t on m.ID_TOPIC = t.ID_TOPIC
		 where m.ID_MSG <> t.ID_FIRST_MSG;
         ", $Comment_Map);

	 // Conversation.
	 $Conv_Map = array(
		'ID_PM' => 'ConversationID',
		'ID_MEMBER_FROM' => 'InsertUserID',
		'DateInserted' => 'DateInserted'
	 );
	 $Ex->ExportTable('Conversation',
		"select *, from_unixtime(msgtime) as DateInserted
		from :_personal_messages
		where deletedBySender = 0", $Conv_Map);

	 // ConversationMessage.
	 $ConvMessage_Map = array(
		'MessageID' => 'MessageID',
		'ConversationID' => 'ConversationID',
		'DateInserted' => 'DateInserted',
		'ID_MEMBER_FROM' => 'InsertUserID',
		'body' => array('Column'=>'Body','Filter'=>'bb2html')
	 );
	 $Ex->ExportTable('ConversationMessage',
		"select *, ID_PM AS MessageID, ID_PM AS ConversationID, from_unixtime(msgtime) as DateInserted
		from :_personal_messages
		where deletedBySender = 0", $ConvMessage_Map);

	 // UserConversation.
	 $UserConv_Map = array(
		'ID_MEMBER' => 'UserID',
		'ConversationID' => 'ConversationID',
		'LastMessageID' => 'LastMessageID'
	 );
	 $Ex->ExportTable('UserConversation',
		"select *, ID_PM AS LastMessageID, ID_PM AS ConversationID
		from :_pm_recipients
		where deletedBySender = 0", $UserConv_Map);

      // End
      $Ex->EndExport();
   }
}

function bb2html($text)
{
  global $txt, $scripturl, $context, $modSettings, $user_info;

  $user_info['time_format'] = '%Y-%m-%d';

  // A bit of language stuff used by the parser
  $txt['lang_character_set'] = 'utf8';
  $txt['smf238'] = 'Code';
  $txt['smf239'] = 'Quote from';
  $txt['smf240'] = 'Quote';
  $txt[176] = 'on';
  $txt['lang_locale'] = 'en';
  $txt[287] = 'Smiley';
  $txt[288] = 'Angry';
  $txt[289] = 'Cheesy';
  $txt[290] = 'Laugh';
  $txt[291] = 'Sad';
  $txt[292] = 'Wink';
  $txt[293] = 'Grin';
  $txt[294] = 'Shocked';
  $txt[295] = 'Cool';
  $txt[296] = 'Huh';
  $txt[450] = 'Roll Eyes';
  $txt[451] = 'Tongue';
  $txt[526] = 'Embarrassed';
  $txt[527] = 'Lips sealed';
  $txt[528] = 'Undecided';
  $txt[529] = 'Kiss';
  $txt[530] = 'Cry';

  // URL stuff
  $scripturl = dirname($_SERVER['REQUEST_URI']); // TODO: make sure this is right
  
  // some settings
  $modSettings['enableBBC'] = true;
  $modSettings['enablePostHTML'] = true;
  $modSettings['max_image_width'] = 500;
  $modSettings['max_image_height'] = 500;
  $modSettings['autoLinkUrls'] = false;
  $modSettings['fixLongWords'] = false;

  return parse_bbc($text, false);
}

// Format a time to make it look purdy.
function timeformat($logTime, $show_today = true)
{
  global $user_info, $txt, $db_prefix, $modSettings, $func;

  // Offset the time.
  $time = $logTime + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;

  // We can't have a negative date (on Windows, at least.)
  if ($time < 0)
    $time = 0;

  // Today and Yesterday?
  if ($modSettings['todayMod'] >= 1 && $show_today === true)
  {
    // Get the current time.
    $nowtime = forum_time();

    $then = @getdate($time);
    $now = @getdate($nowtime);

    // Try to make something of a time format string...
    $s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
    if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
      $today_fmt = '%I:%M' . $s . ' %p';
    else
      $today_fmt = '%H:%M' . $s;

    // Same day of the year, same year.... Today!
    if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
      return $txt['smf10'] . timeformat($logTime, $today_fmt);

    // Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
    if ($modSettings['todayMod'] == '2' && (($then['yday'] == $now['yday'] - 1 && $then['year'] == $now['year']) || ($now['yday'] == 0 && $then['year'] == $now['year'] - 1) && $then['mon'] == 12 && $then['mday'] == 31))
      return $txt['smf10b'] . timeformat($logTime, $today_fmt);
  }

  $str = !is_bool($show_today) ? $show_today : $user_info['time_format'];

  if (setlocale(LC_TIME, $txt['lang_locale']))
  {
    foreach (array('%a', '%A', '%b', '%B') as $token)
      if (strpos($str, $token) !== false)
        $str = str_replace($token, $func['ucwords'](strftime($token, $time)), $str);
  }
  else
  {
    // Do-it-yourself time localization.  Fun.
    foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
      if (strpos($str, $token) !== false)
        $str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
    if (strpos($str, '%p'))
      $str = str_replace('%p', (strftime('%H', $time) < 12 ? 'am' : 'pm'), $str);
  }

  // Format any other characters..
  return strftime($str, $time);
}

// Removes special entities from strings.  Compatibility...
function un_htmlspecialchars($string)
{
  return strtr($string, array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES)) + array('&#039;' => '\'', '&nbsp;' => ' '));
}

// This gets all possible permutations of an array.
function permute($array)
{
  $orders = array($array);

  $n = count($array);
  $p = range(0, $n);
  for ($i = 1; $i < $n; null)
  {
    $p[$i]--;
    $j = $i % 2 != 0 ? $p[$i] : 0;

    $temp = $array[$i];
    $array[$i] = $array[$j];
    $array[$j] = $temp;

    for ($i = 1; $p[$i] == 0; $i++)
      $p[$i] = 1;

    $orders[] = $array;
  }

  return $orders;
}

// Parse bulletin board code in a string, as well as smileys optionally.
function parse_bbc($message, $smileys = true, $cache_id = '')
{
  global $txt, $scripturl, $context, $modSettings, $user_info;
  static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
  static $disabled;

  // Never show smileys for wireless clients.  More bytes, can't see it anyway :P.
  if (WIRELESS)
    $smileys = false;
  elseif ($smileys !== null && ($smileys == '1' || $smileys == '0'))
    $smileys = (bool) $smileys;

  if (empty($modSettings['enableBBC']) && $message !== false)
  {
    if ($smileys === true)
      parsesmileys($message);

    return $message;
  }

  // Just in case it wasn't determined yet whether UTF-8 is enabled.
  if (!isset($context['utf8']))
    $context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';

  // Sift out the bbc for a performance improvement.
  if (empty($bbc_codes) || $message === false)
  {
    if (!empty($modSettings['disabledBBC']))
    {
      $temp = explode(',', strtolower($modSettings['disabledBBC']));

      foreach ($temp as $tag)
        $disabled[trim($tag)] = true;
    }

    if (empty($modSettings['enableEmbeddedFlash']))
      $disabled['flash'] = true;

    /* The following bbc are formatted as an array, with keys as follows:

      tag: the tag's name - should be lowercase!

      type: one of...
        - (missing): [tag]parsed content[/tag]
        - unparsed_equals: [tag=xyz]parsed content[/tag]
        - parsed_equals: [tag=parsed data]parsed content[/tag]
        - unparsed_content: [tag]unparsed content[/tag]
        - closed: [tag], [tag/], [tag /]
        - unparsed_commas: [tag=1,2,3]parsed content[/tag]
        - unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
        - unparsed_equals_content: [tag=...]unparsed content[/tag]

      parameters: an optional array of parameters, for the form
        [tag abc=123]content[/tag].  The array is an associative array
        where the keys are the parameter names, and the values are an
        array which may contain the following:
        - match: a regular expression to validate and match the value.
        - quoted: true if the value should be quoted.
        - validate: callback to evaluate on the data, which is $data.
        - value: a string in which to replace $1 with the data.
          either it or validate may be used, not both.
        - optional: true if the parameter is optional.

      test: a regular expression to test immediately after the tag's
        '=', ' ' or ']'.  Typically, should have a \] at the end.
        Optional.

      content: only available for unparsed_content, closed,
        unparsed_commas_content, and unparsed_equals_content.
        $1 is replaced with the content of  the tag.  Parameters
        are repalced in the form {param}.  For unparsed_commas_content,
        $2, $3, ..., $n are replaced.

      before: only when content is not used, to go before any
        content.  For unparsed_equals, $1 is replaced with the value.
        For unparsed_commas, $1, $2, ..., $n are replaced.

      after: similar to before in every way, except that it is used
        when the tag is closed.

      disabled_content: used in place of content when the tag is
        disabled.  For closed, default is '', otherwise it is '$1' if
        block_level is false, '<div>$1</div>' elsewise.

      disabled_before: used in place of before when disabled.  Defaults
        to '<div>' if block_level, '' if not.

      disabled_after: used in place of after when disabled.  Defaults
        to '</div>' if block_level, '' if not.

      block_level: set to true the tag is a "block level" tag, similar
        to HTML.  Block level tags cannot be nested inside tags that are
        not block level, and will not be implicitly closed as easily.
        One break following a block level tag may also be removed.

      trim: if set, and 'inside' whitespace after the begin tag will be
        removed.  If set to 'outside', whitespace after the end tag will
        meet the same fate.

      validate: except when type is missing or 'closed', a callback to
        validate the data as $data.  Depending on the tag's type, $data
        may be a string or an array of strings (corresponding to the
        replacement.)

      quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
        may be not set, 'optional', or 'required' corresponding to if
        the content may be quoted.  This allows the parser to read
        [tag="abc]def[esdf]"] properly.

      require_parents: an array of tag names, or not set.  If set, the
        enclosing tag *must* be one of the listed tags, or parsing won't
        occur.

      require_children: similar to require_parents, if set children
        won't be parsed if they are not in the list.

      disallow_children: similar to, but very different from,
        require_children, if it is set the listed tags will not be
        parsed inside the tag.
    */

    $codes = array(
      array(
        'tag' => 'abbr',
        'type' => 'unparsed_equals',
        'before' => '<abbr title="$1">',
        'after' => '</abbr>',
        'quoted' => 'optional',
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'acronym',
        'type' => 'unparsed_equals',
        'before' => '<acronym title="$1">',
        'after' => '</acronym>',
        'quoted' => 'optional',
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'anchor',
        'type' => 'unparsed_equals',
        'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
        'before' => '<span id="post_$1" />',
        'after' => '',
      ),
      array(
        'tag' => 'b',
        'before' => '<b>',
        'after' => '</b>',
      ),
      array(
        'tag' => 'black',
        'before' => '<span style="color: black;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'blue',
        'before' => '<span style="color: blue;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'br',
        'type' => 'closed',
        'content' => '<br />',
      ),
      array(
        'tag' => 'code',
        'type' => 'unparsed_content',
        'content' => '<div class="codeheader">' . $txt['smf238'] . ':</div><div class="code">' . ($context['browser']['is_gecko'] ? '<pre style="margin-top: 0; display: inline;">$1</pre>' : '$1') . '</div>',
        // !!! Maybe this can be simplified?
        'validate' => isset($disabled['code']) ? null : create_function('&$tag, &$data, $disabled', '
          global $context;

          if (!isset($disabled[\'code\']))
          {
            $php_parts = preg_split(\'~(&lt;\?php|\?&gt;)~\', $data, -1, PREG_SPLIT_DELIM_CAPTURE);

            for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
            {
              // Do PHP code coloring?
              if ($php_parts[$php_i] != \'&lt;?php\')
                continue;

              $php_string = \'\';
              while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != \'?&gt;\')
              {
                $php_string .= $php_parts[$php_i];
                $php_parts[$php_i++] = \'\';
              }
              $php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
            }

            // Fix the PHP code stuff...
            $data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));

            // Older browsers are annoying, aren\'t they?
            if ($context[\'browser\'][\'is_ie4\'] || $context[\'browser\'][\'is_ie5\'] || $context[\'browser\'][\'is_ie5.5\'])
              $data = str_replace("\t", "<pre style=\"display: inline;\">\t</pre>", $data);
            elseif (!$context[\'browser\'][\'is_gecko\'])
              $data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
          }'),
        'block_level' => true,
      ),
      array(
        'tag' => 'code',
        'type' => 'unparsed_equals_content',
        'content' => '<div class="codeheader">' . $txt['smf238'] . ': ($2)</div><div class="code">' . ($context['browser']['is_gecko'] ? '<pre style="margin-top: 0; display: inline;">$1</pre>' : '$1') . '</div>',
        // !!! Maybe this can be simplified?
        'validate' => isset($disabled['code']) ? null : create_function('&$tag, &$data, $disabled', '
          global $context;

          if (!isset($disabled[\'code\']))
          {
            $php_parts = preg_split(\'~(&lt;\?php|\?&gt;)~\', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);

            for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
            {
              // Do PHP code coloring?
              if ($php_parts[$php_i] != \'&lt;?php\')
                continue;

              $php_string = \'\';
              while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != \'?&gt;\')
              {
                $php_string .= $php_parts[$php_i];
                $php_parts[$php_i++] = \'\';
              }
              $php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
            }

            // Fix the PHP code stuff...
            $data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));

            // Older browsers are annoying, aren\'t they?
            if ($context[\'browser\'][\'is_ie4\'] || $context[\'browser\'][\'is_ie5\'] || $context[\'browser\'][\'is_ie5.5\'])
              $data = str_replace("\t", "<pre style=\"display: inline;\">\t</pre>", $data);
            elseif (!$context[\'browser\'][\'is_gecko\'])
              $data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
          }'),
        'block_level' => true,
      ),
      array(
        'tag' => 'center',
        'before' => '<div align="center">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'color',
        'type' => 'unparsed_equals',
        'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,12})\]',
        'before' => '<span style="color: $1;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'email',
        'type' => 'unparsed_content',
        'content' => '<a href="mailto:$1">$1</a>',
        // !!! Should this respect guest_hideContacts?
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
      ),
      array(
        'tag' => 'email',
        'type' => 'unparsed_equals',
        'before' => '<a href="mailto:$1">',
        'after' => '</a>',
        // !!! Should this respect guest_hideContacts?
        'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'ftp',
        'type' => 'unparsed_content',
        'content' => '<a href="$1" target="_blank">$1</a>',
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
      ),
      array(
        'tag' => 'ftp',
        'type' => 'unparsed_equals',
        'before' => '<a href="$1" target="_blank">',
        'after' => '</a>',
        'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'font',
        'type' => 'unparsed_equals',
        'test' => '[A-Za-z0-9_,\-\s]+?\]',
        'before' => '<span style="font-family: $1;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'flash',
        'type' => 'unparsed_commas_content',
        'test' => '\d+,\d+\]',
        'content' => ($context['browser']['is_ie'] && !$context['browser']['is_mac_ie'] ? '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="$2" height="$3"><param name="movie" value="$1" /><param name="play" value="true" /><param name="loop" value="true" /><param name="quality" value="high" /><param name="AllowScriptAccess" value="never" /><embed src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never" /><noembed><a href="$1" target="_blank">$1</a></noembed></object>' : '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never" /><noembed><a href="$1" target="_blank">$1</a></noembed>'),
        'validate' => create_function('&$tag, &$data, $disabled', '
          if (isset($disabled[\'url\']))
            $tag[\'content\'] = \'$1\';'),
        'disabled_content' => '<a href="$1" target="_blank">$1</a>',
      ),
      array(
        'tag' => 'green',
        'before' => '<span style="color: green;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'glow',
        'type' => 'unparsed_commas',
        'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
        'before' => $context['browser']['is_ie'] ? '<table border="0" cellpadding="0" cellspacing="0" style="display: inline; vertical-align: middle; font: inherit;"><tr><td style="filter: Glow(color=$1, strength=$2); font: inherit;">' : '<span style="background-color: $1;">',
        'after' => $context['browser']['is_ie'] ? '</td></tr></table> ' : '</span>',
      ),
      array(
        'tag' => 'hr',
        'type' => 'closed',
        'content' => '<hr />',
        'block_level' => true,
      ),
      array(
        'tag' => 'html',
        'type' => 'unparsed_content',
        'content' => '$1',
        'block_level' => true,
        'disabled_content' => '$1',
      ),
      array(
        'tag' => 'img',
        'type' => 'unparsed_content',
        'parameters' => array(
          'alt' => array('optional' => true),
          'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
          'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
        ),
        'content' => '<img src="$1" alt="{alt}"{width}{height} border="0" />',
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
        'disabled_content' => '($1)',
      ),
      array(
        'tag' => 'img',
        'type' => 'unparsed_content',
        'content' => '<img src="$1" alt="" border="0" />',
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
        'disabled_content' => '($1)',
      ),
      array(
        'tag' => 'i',
        'before' => '<i>',
        'after' => '</i>',
      ),
      array(
        'tag' => 'iurl',
        'type' => 'unparsed_content',
        'content' => '<a href="$1" rel="nofollow">$1</a>',
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
      ),
      array(
        'tag' => 'iurl',
        'type' => 'unparsed_equals',
        'before' => '<a href="$1" rel="nofollow">',
        'after' => '</a>',
        'validate' => create_function('&$tag, &$data, $disabled', '
          if (substr($data, 0, 1) == \'#\')
            $data = \'#post_\' . substr($data, 1);'),
        'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'li',
        'before' => '<li>',
        'after' => '</li>',
        'trim' => 'outside',
        'require_parents' => array('list'),
        'block_level' => true,
        'disabled_before' => '',
        'disabled_after' => '<br />',
      ),
      array(
        'tag' => 'list',
        'before' => '<ul style="margin-top: 0; margin-bottom: 0;">',
        'after' => '</ul>',
        'trim' => 'inside',
        'require_children' => array('li'),
        'block_level' => true,
      ),
      array(
        'tag' => 'list',
        'parameters' => array(
          'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
        ),
        'before' => '<ul style="margin-top: 0; margin-bottom: 0; list-style-type: {type};">',
        'after' => '</ul>',
        'trim' => 'inside',
        'require_children' => array('li'),
        'block_level' => true,
      ),
      array(
        'tag' => 'left',
        'before' => '<div style="text-align: left;">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'ltr',
        'before' => '<div dir="ltr">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'me',
        'type' => 'unparsed_equals',
        'before' => '<div class="meaction">* $1 ',
        'after' => '</div>',
        'quoted' => 'optional',
        'block_level' => true,
        'disabled_before' => '/me ',
        'disabled_after' => '<br />',
      ),
      array(
        'tag' => 'move',
        'before' => '<marquee>',
        'after' => '</marquee>',
        'block_level' => true,
      ),
      array(
        'tag' => 'nobbc',
        'type' => 'unparsed_content',
        'content' => '$1',
      ),
      array(
        'tag' => 'pre',
        'before' => '<pre>',
        'after' => '</pre>',
      ),
      array(
        'tag' => 'php',
        'type' => 'unparsed_content',
        'content' => '<div class="phpcode">$1</div>',
        'validate' => isset($disabled['php']) ? null : create_function('&$tag, &$data, $disabled', '
          if (!isset($disabled[\'php\']))
          {
            $add_begin = substr(trim($data), 0, 5) != \'&lt;?\';
            $data = highlight_php_code($add_begin ? \'&lt;?php \' . $data . \'?&gt;\' : $data);
            if ($add_begin)
              $data = preg_replace(array(\'~^(.+?)&lt;\?.{0,40}?php(&nbsp;|\s)~\', \'~\?&gt;((?:</(font|span)>)*)$~\'), \'$1\', $data, 2);
          }'),
        'block_level' => true,
        'disabled_content' => '$1',
      ),
      array(
        'tag' => 'quote',
        'before' => '<div class="quoteheader">' . $txt['smf240'] . '</div><div class="quote">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'quote',
        'parameters' => array(
          'author' => array('match' => '(.{1,192}?)', 'quoted' => true, 'validate' => 'parse_bbc'),
        ),
        'before' => '<div class="quoteheader">' . $txt['smf239'] . ': {author}</div><div class="quote">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'quote',
        'type' => 'parsed_equals',
        'before' => '<div class="quoteheader">' . $txt['smf239'] . ': $1</div><div class="quote">',
        'after' => '</div>',
        'quoted' => 'optional',
        'block_level' => true,
      ),
      array(
        'tag' => 'quote',
        'parameters' => array(
          'author' => array('match' => '([^<>]{1,192}?)'),
          'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|action=profile;u=\d+)'),
          'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
        ),
        'before' => '<div class="quoteheader"><a href="' . $scripturl . '?{link}">' . $txt['smf239'] . ': {author} ' . $txt[176] . ' {date}</a></div><div class="quote">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'quote',
        'parameters' => array(
          'author' => array('match' => '(.{1,192}?)', 'validate' => 'parse_bbc'),
        ),
        'before' => '<div class="quoteheader">' . $txt['smf239'] . ': {author}</div><div class="quote">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'right',
        'before' => '<div style="text-align: right;">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 'red',
        'before' => '<span style="color: red;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'rtl',
        'before' => '<div dir="rtl">',
        'after' => '</div>',
        'block_level' => true,
      ),
      array(
        'tag' => 's',
        'before' => '<del>',
        'after' => '</del>',
      ),
      array(
        'tag' => 'size',
        'type' => 'unparsed_equals',
        'test' => '([1-9][\d]?p[xt]|(?:x-)?small(?:er)?|(?:x-)?large[r]?)\]',
        // !!! line-height
        'before' => '<span style="font-size: $1; line-height: 1.3em;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'size',
        'type' => 'unparsed_equals',
        'test' => '[1-9]\]',
        // !!! line-height
        'before' => '<font size="$1" style="line-height: 1.3em;">',
        'after' => '</font>',
      ),
      array(
        'tag' => 'sub',
        'before' => '<sub>',
        'after' => '</sub>',
      ),
      array(
        'tag' => 'sup',
        'before' => '<sup>',
        'after' => '</sup>',
      ),
      array(
        'tag' => 'shadow',
        'type' => 'unparsed_commas',
        'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
        'before' => $context['browser']['is_ie'] ? '<span style="filter: Shadow(color=$1, direction=$2); height: 1.2em;\">' : '<span style="text-shadow: $1 $2">',
        'after' => '</span>',
        'validate' => $context['browser']['is_ie'] ? create_function('&$tag, &$data, $disabled', '
          if ($data[1] == \'left\')
            $data[1] = 270;
          elseif ($data[1] == \'right\')
            $data[1] = 90;
          elseif ($data[1] == \'top\')
            $data[1] = 0;
          elseif ($data[1] == \'bottom\')
            $data[1] = 180;
          else
            $data[1] = (int) $data[1];') : create_function('&$tag, &$data, $disabled', '
          if ($data[1] == \'top\' || (is_numeric($data[1]) && $data[1] < 50))
            return \'0 -2px\';
          elseif ($data[1] == \'right\' || (is_numeric($data[1]) && $data[1] < 100))
            return \'2px 0\';
          elseif ($data[1] == \'bottom\' || (is_numeric($data[1]) && $data[1] < 190))
            return \'0 2px\';
          elseif ($data[1] == \'left\' || (is_numeric($data[1]) && $data[1] < 280))
            return \'-2px 0\';
          else
            return \'0 0\';'),
      ),
      array(
        'tag' => 'time',
        'type' => 'unparsed_content',
        'content' => '$1',
        'validate' => create_function('&$tag, &$data, $disabled', '
          if (is_numeric($data))
            $data = timeformat($data);
          else
            $tag[\'content\'] = \'[time]$1[/time]\';'),
      ),
      array(
        'tag' => 'tt',
        'before' => '<tt>',
        'after' => '</tt>',
      ),
      array(
        'tag' => 'table',
        'before' => '<table style="font: inherit; color: inherit;">',
        'after' => '</table>',
        'trim' => 'inside',
        'require_children' => array('tr'),
        'block_level' => true,
      ),
      array(
        'tag' => 'tr',
        'before' => '<tr>',
        'after' => '</tr>',
        'require_parents' => array('table'),
        'require_children' => array('td'),
        'trim' => 'both',
        'block_level' => true,
        'disabled_before' => '',
        'disabled_after' => '',
      ),
      array(
        'tag' => 'td',
        'before' => '<td valign="top" style="font: inherit; color: inherit;">',
        'after' => '</td>',
        'require_parents' => array('tr'),
        'trim' => 'outside',
        'block_level' => true,
        'disabled_before' => '',
        'disabled_after' => '',
      ),
      array(
        'tag' => 'url',
        'type' => 'unparsed_content',
        'content' => '<a href="$1" rel="nofollow" target="_blank">$1</a>',
        'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br />\' => \'\'));'),
      ),
      array(
        'tag' => 'url',
        'type' => 'unparsed_equals',
        'before' => '<a href="$1" rel="nofollow" target="_blank">',
        'after' => '</a>',
        'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
        'disabled_after' => ' ($1)',
      ),
      array(
        'tag' => 'u',
        'before' => '<span style="text-decoration: underline;">',
        'after' => '</span>',
      ),
      array(
        'tag' => 'white',
        'before' => '<span style="color: white;">',
        'after' => '</span>',
      ),
    );

    // This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
    if ($message === false)
      return $codes;

    // So the parser won't skip them.
    $itemcodes = array(
      '*' => '',
      '@' => 'disc',
      '+' => 'square',
      'x' => 'square',
      '#' => 'square',
      'o' => 'circle',
      'O' => 'circle',
      '0' => 'circle',
    );
    if (!isset($disabled['li']) && !isset($disabled['list']))
    {
      foreach ($itemcodes as $c => $dummy)
        $bbc_codes[$c] = array();
    }

    // Inside these tags autolink is not recommendable.
    $no_autolink_tags = array(
      'url',
      'iurl',
      'ftp',
      'email',
    );

    // Shhhh!
    if (!isset($disabled['color']))
    {
      $codes[] = array(
        'tag' => 'chrissy',
        'before' => '<span style="color: #CC0099;">',
        'after' => ' :-*</span>',
      );
      $codes[] = array(
        'tag' => 'kissy',
        'before' => '<span style="color: #CC0099;">',
        'after' => ' :-*</span>',
      );
    }

    foreach ($codes as $c)
      $bbc_codes[substr($c['tag'], 0, 1)][] = $c;
    $codes = null;
  }

  // Shall we take the time to cache this?
  if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && strlen($message) > 1000) || strlen($message) > 2400))
  {
    // It's likely this will change if the message is modified.
    $cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . serialize($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);

    if (($temp = cache_get_data($cache_key, 240)) != null)
      return $temp;

    $cache_t = microtime();
  }

  if ($smileys === 'print')
  {
    // [glow], [shadow], and [move] can't really be printed.
    $disabled['glow'] = true;
    $disabled['shadow'] = true;
    $disabled['move'] = true;

    // Colors can't well be displayed... supposed to be black and white.
    $disabled['color'] = true;
    $disabled['black'] = true;
    $disabled['blue'] = true;
    $disabled['white'] = true;
    $disabled['red'] = true;
    $disabled['green'] = true;
    $disabled['me'] = true;

    // Color coding doesn't make sense.
    $disabled['php'] = true;

    // Links are useless on paper... just show the link.
    $disabled['ftp'] = true;
    $disabled['url'] = true;
    $disabled['iurl'] = true;
    $disabled['email'] = true;
    $disabled['flash'] = true;

    // !!! Change maybe?
    if (!isset($_GET['images']))
      $disabled['img'] = true;

    // !!! Interface/setting to add more?
  }

  $open_tags = array();
  $message = strtr($message, array("\n" => '<br />'));

  // The non-breaking-space looks a bit different each time.
  $non_breaking_space = $context['utf8'] ? ($context['server']['complex_preg_chars'] ? '\x{C2A0}' : chr(0xC2) . chr(0xA0)) : '\xA0';

  $pos = -1;
  while ($pos !== false)
  {
    $last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
    $pos = strpos($message, '[', $pos + 1);

    // Failsafe.
    if ($pos === false || $last_pos > $pos)
      $pos = strlen($message) + 1;

    // Can't have a one letter smiley, URL, or email! (sorry.)
    if ($last_pos < $pos - 1)
    {
      // We want to eat one less, and one more, character (for smileys.)
      $last_pos = max($last_pos - 1, 0);
      $data = substr($message, $last_pos, $pos - $last_pos + 1);

      // Take care of some HTML!
      if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
      {
        $data = preg_replace('~&lt;a\s+href=(?:&quot;)?((?:http://|ftp://|https://|ftps://|mailto:).+?)(?:&quot;)?&gt;~i', '[url=$1]', $data);
        $data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);

        // <br /> should be empty.
        $empty_tags = array('br', 'hr');
        foreach ($empty_tags as $tag)
          $data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);

        // b, u, i, s, pre... basic tags.
        $closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
        foreach ($closable_tags as $tag)
        {
          $diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
          $data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));

          if ($diff > 0)
            $data .= str_repeat('</' . $tag . '>', $diff);
        }

        // Do <img ... /> - with security... action= -> action-.
        preg_match_all('~&lt;img\s+src=(?:&quot;)?((?:http://|ftp://|https://|ftps://).+?)(?:&quot;)?(?:\s+alt=(?:&quot;)?(.*?)(?:&quot;)?)?(?:\s?/)?&gt;~i', $data, $matches, PREG_PATTERN_ORDER);
        if (!empty($matches[0]))
        {
          $replaces = array();
          foreach ($matches[1] as $match => $imgtag)
          {
            // No alt?
            if (!isset($matches[2][$match]))
              $matches[2][$match] = '';

            // Remove action= from the URL - no funny business, now.
            if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
              $imgtag = preg_replace('~action(=|%3d)(?!dlattach)~i', 'action-', $imgtag);

            // Check if the image is larger than allowed.
            if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
            {
              list ($width, $height) = url_image_size($imgtag);

              if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
              {
                $height = (int) (($modSettings['max_image_width'] * $height) / $width);
                $width = $modSettings['max_image_width'];
              }

              if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
              {
                $width = (int) (($modSettings['max_image_height'] * $width) / $height);
                $height = $modSettings['max_image_height'];
              }

              // Set the new image tag.
              $replaces[$matches[0][$match]] = '<img src="' . $imgtag . '" width="' . $width . '" height="' . $height . '" alt="' . $matches[2][$match] . '" border="0" />';
            }
            else
              $replaces[$matches[0][$match]] = '<img src="' . $imgtag . '" alt="' . $matches[2][$match] . '" border="0" />';
          }

          $data = strtr($data, $replaces);
        }
      }

      if (!empty($modSettings['autoLinkUrls']))
      {
        // Are we inside tags that should be auto linked?
        $no_autolink_area = false;
        if (!empty($open_tags))
        {
          foreach ($open_tags as $open_tag)
            if (in_array($open_tag['tag'], $no_autolink_tags))
              $no_autolink_area = true;
        }

        // Don't go backwards.
        //!!! Don't think is the real solution....
        $lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
        if ($pos < $lastAutoPos)
          $no_autolink_area = true;
        $lastAutoPos = $pos;

        if (!$no_autolink_area)
        {
          // Parse any URLs.... have to get rid of the @ problems some things cause... stupid email addresses.
          if (!isset($disabled['url']) && (strpos($data, '://') !== false || strpos($data, 'www.') !== false))
          {
            // Switch out quotes really quick because they can cause problems.
            $data = strtr($data, array('&#039;' => '\'', '&nbsp;' => $context['utf8'] ? "\xC2\xA0" : "\xA0", '&quot;' => '>">', '"' => '<"<', '&lt;' => '<lt<'));
            $data = preg_replace(array('~(?<=[\s>\.(;\'"]|^)((?:http|https|ftp|ftps)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\w\-_\~%\.@,\?&;=#+:\'\\\\]*|[\(\{][\w\-_\~%\.@,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\])~i', '~(?<=[\s>(\'<]|^)(www(?:\.[\w\-_]+)+(?::\d+)?(?:/[\w\-_\~%\.@,\?&;=#+:\'\\\\]*|[\(\{][\w\-_\~%\.@,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\])~i'), array('[url]$1[/url]', '[url=http://$1]$1[/url]'), $data);
            $data = strtr($data, array('\'' => '&#039;', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;', '>">' => '&quot;', '<"<' => '"', '<lt<' => '&lt;'));
          }

          // Next, emails...
          if (!isset($disabled['email']) && strpos($data, '@') !== false)
          {
            $data = preg_replace('~(?<=[\?\s' . $non_breaking_space . '\[\]()*\\\;>]|^)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?,\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />|&nbsp;|&gt;|&lt;|&quot;|&#039;|\.(?:\.|;|&nbsp;|\s|$|<br />))~' . ($context['utf8'] ? 'u' : ''), '[email]$1[/email]', $data);
            $data = preg_replace('~(?<=<br />)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?\.,;\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />|&nbsp;|&gt;|&lt;|&quot;|&#039;)~' . ($context['utf8'] ? 'u' : ''), '[email]$1[/email]', $data);
          }
        }
      }

      $data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));

      if (!empty($modSettings['fixLongWords']) && $modSettings['fixLongWords'] > 5)
      {
        // This is SADLY and INCREDIBLY browser dependent.
        if ($context['browser']['is_gecko'] || $context['browser']['is_konqueror'])
          $breaker = '<span style="margin: 0 -0.5ex 0 0;"> </span>';
        // Opera...
        elseif ($context['browser']['is_opera'])
          $breaker = '<span style="margin: 0 -0.65ex 0 -1px;"> </span>';
        // Internet Explorer...
        else
          $breaker = '<span style="width: 0; margin: 0 -0.6ex 0 -1px;"> </span>';

        // PCRE will not be happy if we don't give it a short.
        $modSettings['fixLongWords'] = (int) min(65535, $modSettings['fixLongWords']);

        // The idea is, find words xx long, and then replace them with xx + space + more.
        if (strlen($data) > $modSettings['fixLongWords'])
        {
          // This is done in a roundabout way because $breaker has "long words" :P.
          $data = strtr($data, array($breaker => '< >', '&nbsp;' => $context['utf8'] ? "\xC2\xA0" : "\xA0"));
          $data = preg_replace(
            '~(?<=[>;:!? ' . $non_breaking_space . '\]()]|^)([\w\.]{' . $modSettings['fixLongWords'] . ',})~e' . ($context['utf8'] ? 'u' : ''),
            "preg_replace('/(.{" . ($modSettings['fixLongWords'] - 1) . '})/' . ($context['utf8'] ? 'u' : '') . "', '\\\$1< >', '\$1')",
            $data);
          $data = strtr($data, array('< >' => $breaker, $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
        }
      }

      // Do any smileys!
      if ($smileys === true)
        parsesmileys($data);

      // If it wasn't changed, no copying or other boring stuff has to happen!
      if ($data != substr($message, $last_pos, $pos - $last_pos + 1))
      {
        $message = substr($message, 0, $last_pos) . $data . substr($message, $pos + 1);

        // Since we changed it, look again incase we added or removed a tag.  But we don't want to skip any.
        $old_pos = strlen($data) + $last_pos - 1;
        $pos = strpos($message, '[', $last_pos);
        $pos = $pos === false ? $old_pos : min($pos, $old_pos);
      }
    }

    // Are we there yet?  Are we there yet?
    if ($pos >= strlen($message) - 1)
      break;

    $tags = strtolower(substr($message, $pos + 1, 1));

    if ($tags == '/' && !empty($open_tags))
    {
      $pos2 = strpos($message, ']', $pos + 1);
      if ($pos2 == $pos + 2)
        continue;
      $look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));

      $to_close = array();
      $block_level = null;
      do
      {
        $tag = array_pop($open_tags);
        if (!$tag)
          break;

        if (!empty($tag['block_level']))
        {
          // Only find out if we need to.
          if ($block_level === false)
          {
            array_push($open_tags, $tag);
            break;
          }

          // The idea is, if we are LOOKING for a block level tag, we can close them on the way.
          if (strlen($look_for) > 0 && isset($bbc_codes[$look_for{0}]))
          {
            foreach ($bbc_codes[$look_for{0}] as $temp)
              if ($temp['tag'] == $look_for)
              {
                $block_level = !empty($temp['block_level']);
                break;
              }
          }

          if ($block_level !== true)
          {
            $block_level = false;
            array_push($open_tags, $tag);
            break;
          }
        }

        $to_close[] = $tag;
      }
      while ($tag['tag'] != $look_for);

      // Did we just eat through everything and not find it?
      if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
      {
        $open_tags = $to_close;
        continue;
      }
      elseif (!empty($to_close) && $tag['tag'] != $look_for)
      {
        if ($block_level === null && isset($look_for{0}, $bbc_codes[$look_for{0}]))
        {
          foreach ($bbc_codes[$look_for{0}] as $temp)
            if ($temp['tag'] == $look_for)
            {
              $block_level = !empty($temp['block_level']);
              break;
            }
        }

        // We're not looking for a block level tag (or maybe even a tag that exists...)
        if (!$block_level)
        {
          foreach ($to_close as $tag)
            array_push($open_tags, $tag);
          continue;
        }
      }

      foreach ($to_close as $tag)
      {
        $message = substr($message, 0, $pos) . $tag['after'] . substr($message, $pos2 + 1);
        $pos += strlen($tag['after']);
        $pos2 = $pos - 1;

        // See the comment at the end of the big loop - just eating whitespace ;).
        if (!empty($tag['block_level']) && substr($message, $pos, 6) == '<br />')
          $message = substr($message, 0, $pos) . substr($message, $pos + 6);
        if (!empty($tag['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br />|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
          $message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
      }

      if (!empty($to_close))
      {
        $to_close = array();
        $pos--;
      }

      continue;
    }

    // No tags for this character, so just keep going (fastest possible course.)
    if (!isset($bbc_codes[$tags]))
      continue;

    $inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
    $tag = null;
    foreach ($bbc_codes[$tags] as $possible)
    {
      // Not a match?
      if (strtolower(substr($message, $pos + 1, strlen($possible['tag']))) != $possible['tag'])
        continue;

      $next_c = substr($message, $pos + 1 + strlen($possible['tag']), 1);

      // A test validation?
      if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + strlen($possible['tag']) + 1)) == 0)
        continue;
      // Do we want parameters?
      elseif (!empty($possible['parameters']))
      {
        if ($next_c != ' ')
          continue;
      }
      elseif (isset($possible['type']))
      {
        // Do we need an equal sign?
        if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
          continue;
        // Maybe we just want a /...
        if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + strlen($possible['tag']), 2) != '/]' && substr($message, $pos + 1 + strlen($possible['tag']), 3) != ' /]')
          continue;
        // An immediate ]?
        if ($possible['type'] == 'unparsed_content' && $next_c != ']')
          continue;
      }
      // No type means 'parsed_content', which demands an immediate ] without parameters!
      elseif ($next_c != ']')
        continue;

      // Check allowed tree?
      if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
        continue;
      elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
        continue;
      // If this is in the list of disallowed child tags, don't parse it.
      elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
        continue;

      $pos1 = $pos + 1 + strlen($possible['tag']) + 1;

      // This is long, but it makes things much easier and cleaner.
      if (!empty($possible['parameters']))
      {
        $preg = array();
        foreach ($possible['parameters'] as $p => $info)
          $preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . ')' . (empty($info['optional']) ? '' : '?');

        // Okay, this may look ugly and it is, but it's not going to happen much and it is the best way of allowing any order of parameters but still parsing them right.
        $match = false;
        $orders = permute($preg);
        foreach ($orders as $p)
          if (preg_match('~^' . implode('', $p) . '\]~i', substr($message, $pos1 - 1), $matches) != 0)
          {
            $match = true;
            break;
          }

        // Didn't match our parameter list, try the next possible.
        if (!$match)
          continue;

        $params = array();
        for ($i = 1, $n = count($matches); $i < $n; $i += 2)
        {
          $key = strtok(ltrim($matches[$i]), '=');
          if (isset($possible['parameters'][$key]['value']))
            $params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
          elseif (isset($possible['parameters'][$key]['validate']))
            $params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
          else
            $params['{' . $key . '}'] = $matches[$i + 1];

          // Just to make sure: replace any $ or { so they can't interpolate wrongly.
          $params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
        }

        foreach ($possible['parameters'] as $p => $info)
        {
          if (!isset($params['{' . $p . '}']))
            $params['{' . $p . '}'] = '';
        }

        $tag = $possible;

        // Put the parameters into the string.
        if (isset($tag['before']))
          $tag['before'] = strtr($tag['before'], $params);
        if (isset($tag['after']))
          $tag['after'] = strtr($tag['after'], $params);
        if (isset($tag['content']))
          $tag['content'] = strtr($tag['content'], $params);

        $pos1 += strlen($matches[0]) - 1;
      }
      else
        $tag = $possible;
      break;
    }

    // Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
    if ($smileys !== false && $tag === null && isset($itemcodes[substr($message, $pos + 1, 1)]) && substr($message, $pos + 2, 1) == ']' && !isset($disabled['list']) && !isset($disabled['li']))
    {
      if (substr($message, $pos + 1, 1) == '0' && !in_array(substr($message, $pos - 1, 1), array(';', ' ', "\t", '>')))
        continue;
      $tag = $itemcodes[substr($message, $pos + 1, 1)];

      // First let's set up the tree: it needs to be in a list, or after an li.
      if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
      {
        $open_tags[] = array(
          'tag' => 'list',
          'after' => '</ul>',
          'block_level' => true,
          'require_children' => array('li'),
          'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
        );
        $code = '<ul style="margin-top: 0; margin-bottom: 0;">';
      }
      // We're in a list item already: another itemcode?  Close it first.
      elseif ($inside['tag'] == 'li')
      {
        array_pop($open_tags);
        $code = '</li>';
      }
      else
        $code = '';

      // Now we open a new tag.
      $open_tags[] = array(
        'tag' => 'li',
        'after' => '</li>',
        'trim' => 'outside',
        'block_level' => true,
        'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
      );

      // First, open the tag...
      $code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
      $message = substr($message, 0, $pos) . $code . substr($message, $pos + 3);
      $pos += strlen($code) - 1;

      // Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
      $pos2 = strpos($message, '<br />', $pos);
      $pos3 = strpos($message, '[/', $pos);
      if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
      {
        preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr($message, $pos2 + 6), $matches);
        $message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);

        $open_tags[count($open_tags) - 2]['after'] = '</ul>';
      }
      // Tell the [list] that it needs to close specially.
      else
      {
        // Move the li over, because we're not sure what we'll hit.
        $open_tags[count($open_tags) - 1]['after'] = '';
        $open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
      }

      continue;
    }

    // Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
    if ($tag === null && $inside !== null && !empty($inside['require_children']))
    {
      array_pop($open_tags);

      $message = substr($message, 0, $pos) . $inside['after'] . substr($message, $pos);
      $pos += strlen($inside['after']) - 1;
    }

    // No tag?  Keep looking, then.  Silly people using brackets without actual tags.
    if ($tag === null)
      continue;

    // Propagate the list to the child (so wrapping the disallowed tag won't work either.)
    if (isset($inside['disallow_children']))
      $tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];

    // Is this tag disabled?
    if (isset($disabled[$tag['tag']]))
    {
      if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
      {
        $tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
        $tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
        $tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
      }
      elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
      {
        $tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
        $tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
      }
      else
        $tag['content'] = $tag['disabled_content'];
    }

    // The only special case is 'html', which doesn't need to close things.
    if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
    {
      $n = count($open_tags) - 1;
      while (empty($open_tags[$n]['block_level']) && $n >= 0)
        $n--;

      // Close all the non block level tags so this tag isn't surrounded by them.
      for ($i = count($open_tags) - 1; $i > $n; $i--)
      {
        $message = substr($message, 0, $pos) . $open_tags[$i]['after'] . substr($message, $pos);
        $pos += strlen($open_tags[$i]['after']);
        $pos1 += strlen($open_tags[$i]['after']);

        // Trim or eat trailing stuff... see comment at the end of the big loop.
        if (!empty($open_tags[$i]['block_level']) && substr($message, $pos, 6) == '<br />')
          $message = substr($message, 0, $pos) . substr($message, $pos + 6);
        if (!empty($open_tags[$i]['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br />|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
          $message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));

        array_pop($open_tags);
      }
    }

    // No type means 'parsed_content'.
    if (!isset($tag['type']))
    {
      // !!! Check for end tag first, so people can say "I like that [i] tag"?
      $open_tags[] = $tag;
      $message = substr($message, 0, $pos) . $tag['before'] . substr($message, $pos1);
      $pos += strlen($tag['before']) - 1;
    }
    // Don't parse the content, just skip it.
    elseif ($tag['type'] == 'unparsed_content')
    {
      $pos2 = stripos($message, '[/' . substr($message, $pos + 1, strlen($tag['tag'])) . ']', $pos1);
      if ($pos2 === false)
        continue;

      $data = substr($message, $pos1, $pos2 - $pos1);

      if (!empty($tag['block_level']) && substr($data, 0, 6) == '<br />')
        $data = substr($data, 6);

      if (isset($tag['validate']))
        $tag['validate']($tag, $data, $disabled);

      $code = strtr($tag['content'], array('$1' => $data));
      $message = substr($message, 0, $pos) . $code . substr($message, $pos2 + 3 + strlen($tag['tag']));
      $pos += strlen($code) - 1;
    }
    // Don't parse the content, just skip it.
    elseif ($tag['type'] == 'unparsed_equals_content')
    {
      // The value may be quoted for some tags - check.
      if (isset($tag['quoted']))
      {
        $quoted = substr($message, $pos1, 6) == '&quot;';
        if ($tag['quoted'] != 'optional' && !$quoted)
          continue;

        if ($quoted)
          $pos1 += 6;
      }
      else
        $quoted = false;

      $pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
      if ($pos2 === false)
        continue;
      $pos3 = stripos($message, '[/' . substr($message, $pos + 1, strlen($tag['tag'])) . ']', $pos2);
      if ($pos3 === false)
        continue;

      $data = array(
        substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
        substr($message, $pos1, $pos2 - $pos1)
      );

      if (!empty($tag['block_level']) && substr($data[0], 0, 6) == '<br />')
        $data[0] = substr($data[0], 6);

      // Validation for my parking, please!
      if (isset($tag['validate']))
        $tag['validate']($tag, $data, $disabled);

      $code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
      $message = substr($message, 0, $pos) . $code . substr($message, $pos3 + 3 + strlen($tag['tag']));
      $pos += strlen($code) - 1;
    }
    // A closed tag, with no content or value.
    elseif ($tag['type'] == 'closed')
    {
      $pos2 = strpos($message, ']', $pos);
      $message = substr($message, 0, $pos) . $tag['content'] . substr($message, $pos2 + 1);
      $pos += strlen($tag['content']) - 1;
    }
    // This one is sorta ugly... :/.  Unforunately, it's needed for flash.
    elseif ($tag['type'] == 'unparsed_commas_content')
    {
      $pos2 = strpos($message, ']', $pos1);
      if ($pos2 === false)
        continue;
      $pos3 = stripos($message, '[/' . substr($message, $pos + 1, strlen($tag['tag'])) . ']', $pos2);
      if ($pos3 === false)
        continue;

      // We want $1 to be the content, and the rest to be csv.
      $data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
      $data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);

      if (isset($tag['validate']))
        $tag['validate']($tag, $data, $disabled);

      $code = $tag['content'];
      foreach ($data as $k => $d)
        $code = strtr($code, array('$' . ($k + 1) => trim($d)));
      $message = substr($message, 0, $pos) . $code . substr($message, $pos3 + 3 + strlen($tag['tag']));
      $pos += strlen($code) - 1;
    }
    // This has parsed content, and a csv value which is unparsed.
    elseif ($tag['type'] == 'unparsed_commas')
    {
      $pos2 = strpos($message, ']', $pos1);
      if ($pos2 === false)
        continue;

      $data = explode(',', substr($message, $pos1, $pos2 - $pos1));

      if (isset($tag['validate']))
        $tag['validate']($tag, $data, $disabled);

      // Fix after, for disabled code mainly.
      foreach ($data as $k => $d)
        $tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));

      $open_tags[] = $tag;

      // Replace them out, $1, $2, $3, $4, etc.
      $code = $tag['before'];
      foreach ($data as $k => $d)
        $code = strtr($code, array('$' . ($k + 1) => trim($d)));
      $message = substr($message, 0, $pos) . $code . substr($message, $pos2 + 1);
      $pos += strlen($code) - 1;
    }
    // A tag set to a value, parsed or not.
    elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
    {
      // The value may be quoted for some tags - check.
      if (isset($tag['quoted']))
      {
        $quoted = substr($message, $pos1, 6) == '&quot;';
        if ($tag['quoted'] != 'optional' && !$quoted)
          continue;

        if ($quoted)
          $pos1 += 6;
      }
      else
        $quoted = false;

      $pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
      if ($pos2 === false)
        continue;

      $data = substr($message, $pos1, $pos2 - $pos1);

      // Validation for my parking, please!
      if (isset($tag['validate']))
        $tag['validate']($tag, $data, $disabled);

      // For parsed content, we must recurse to avoid security problems.
      if ($tag['type'] != 'unparsed_equals')
        $data = parse_bbc($data);

      $tag['after'] = strtr($tag['after'], array('$1' => $data));

      $open_tags[] = $tag;

      $code = strtr($tag['before'], array('$1' => $data));
      $message = substr($message, 0, $pos) . $code . substr($message, $pos2 + ($quoted == false ? 1 : 7));
      $pos += strlen($code) - 1;
    }

    // If this is block level, eat any breaks after it.
    if (!empty($tag['block_level']) && substr($message, $pos + 1, 6) == '<br />')
      $message = substr($message, 0, $pos + 1) . substr($message, $pos + 7);

    // Are we trimming outside this tag?
    if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br />|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
      $message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
  }

  // Close any remaining tags.
  while ($tag = array_pop($open_tags))
    $message .= $tag['after'];

  if (substr($message, 0, 1) == ' ')
    $message = '&nbsp;' . substr($message, 1);

  // Cleanup whitespace.
  $message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br />', '<br /> ' => '<br />&nbsp;', '&#13;' => "\n"));

  // Cache the output if it took some time...
  if (isset($cache_key, $cache_t) && array_sum(explode(' ', microtime())) - array_sum(explode(' ', $cache_t)) > 0.05)
    cache_put_data($cache_key, $message, 240);

  return $message;
}

// Parse smileys in the passed message.
function parsesmileys(&$message)
{
  global $modSettings, $db_prefix, $txt, $user_info, $context;
  static $smileyfromcache = array(), $smileytocache = array();

  // No smiley set at all?!
  if ($user_info['smiley_set'] == 'none')
    return;

  // If the smiley array hasn't been set, do it now.
  if (empty($smileyfromcache))
  {
    // Use the default smileys if it is disabled. (better for "portability" of smileys.)
    if (empty($modSettings['smiley_enable']))
    {
      $smileysfrom = array('>:D', ':D', '::)', '>:(', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
      $smileysto = array('evil.gif', 'cheesy.gif', 'rolleyes.gif', 'angry.gif', 'smiley.gif', 'wink.gif', 'grin.gif', 'sad.gif', 'shocked.gif', 'cool.gif', 'tongue.gif', 'huh.gif', 'embarrassed.gif', 'lipsrsealed.gif', 'kiss.gif', 'cry.gif', 'undecided.gif', 'azn.gif', 'afro.gif', 'police.gif', 'angel.gif');
      $smileysdescs = array('', $txt[289], $txt[450], $txt[288], $txt[287], $txt[292], $txt[293], $txt[291], $txt[294], $txt[295], $txt[451], $txt[296], $txt[526], $txt[527], $txt[529], $txt[530], $txt[528], '', '', '', '');
    }
    else
    {
      // Load the smileys in reverse order by length so they don't get parsed wrong.
      if (($temp = cache_get_data('parsing_smileys', 480)) == null)
      {
        $result = db_query("
          SELECT code, filename, description
          FROM {$db_prefix}smileys", __FILE__, __LINE__);
        $smileysfrom = array();
        $smileysto = array();
        $smileysdescs = array();
        while ($row = mysql_fetch_assoc($result))
        {
          $smileysfrom[] = $row['code'];
          $smileysto[] = $row['filename'];
          $smileysdescs[] = $row['description'];
        }
        mysql_free_result($result);

        cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
      }
      else
        list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
    }

    // The non-breaking-space is a complex thing...
    $non_breaking_space = $context['utf8'] ? ($context['server']['complex_preg_chars'] ? '\x{A0}' : pack('C*', 0xC2, 0xA0)) : '\xA0';

    // This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley)
    for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
    {
      $smileyfromcache[] = '/(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|^)(' . preg_quote($smileysfrom[$i], '/') . '|' . preg_quote(htmlspecialchars($smileysfrom[$i], ENT_QUOTES), '/') . ')(?=[^[:alpha:]0-9]|$)/' . ($context['utf8'] ? 'u' : '');
      // Escape a bunch of smiley-related characters in the description so it doesn't get a double dose :P.
      $smileytocache[] = '<img src="' . $modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/' . $smileysto[$i] . '" alt="' . strtr(htmlspecialchars($smileysdescs[$i]), array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')) . '" border="0" />';
    }
  }

  // Replace away!
  // !!! There must be a way to speed this up.
  $message = preg_replace($smileyfromcache, $smileytocache, $message);
}

// Highlight any code...
function highlight_php_code($code)
{
  global $context;

  // Remove special characters.
  $code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));

  $oldlevel = error_reporting(0);

  // It's easier in 4.2.x+.
  if (@version_compare(PHP_VERSION, '4.2.0') == -1)
  {
    ob_start();
    @highlight_string($code);
    $buffer = str_replace(array("\n", "\r"), '', ob_get_contents());
    ob_end_clean();
  }
  else
    $buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));

  error_reporting($oldlevel);

  // Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
  $buffer = preg_replace('~SMF_TAB(</(font|span)><(font color|span style)="[^"]*?">)?\(\);~', "<pre style=\"display: inline;\">\t</pre>", $buffer);

  return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
}


?><?php


// Make sure a default time zone is set
if (ini_get('date.timezone') == '')
   date_default_timezone_set('America/Montreal');

// Instantiate the appropriate controller or display the input page.
if(isset($_POST['type']) && array_key_exists($_POST['type'], $Supported)) {
   // Mini-Factory
   $class = ucwords($_POST['type']);
   $Controller = new $class();
   $Controller->DoExport();
}
else {
   $CanWrite = TestWrite();
   ViewForm(array('Supported' => $Supported, 'CanWrite' => $CanWrite));
}

/**
 * Write out a value passed as bytes to its most readable format.
 */
function FormatMemorySize($Bytes, $Precision = 1) {
   $Units = array('B', 'K', 'M', 'G', 'T');

   $Bytes = max((int)$Bytes, 0);
   $Pow = floor(($Bytes ? log($Bytes) : 0) / log(1024));
   $Pow = min($Pow, count($Units) - 1);

   $Bytes /= pow(1024, $Pow);

   $Result = round($Bytes, $Precision).$Units[$Pow];
   return $Result;
}

/** 
 * Test filesystem permissions 
 */  
function TestWrite() {
   // Create file
   $file = 'vanilla2test.txt';
   @touch($file);
   if(is_writable($file)) {
      @unlink($file);
      return true;
   }
   else return false;
}
?>
