<?php
/**
* @brief Session Handler
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 6th Sept 2013
*/
namespace IPSUtf8;
/**
* Session Handler
*/
class Session
{
/**
* @brief Tables
*/
public $tables = NULL;
/**
* @brief Singleton Instance
*/
protected static $instance = NULL;
/**
* @brief Database Table Columns
*/
protected static $tableTableColumns = array(
array(
'name' => 'table_name',
'type' => 'TEXT',
'length' => false,
'null' => false,
'default' => null
),
array(
'name' => 'table_schema',
'type' => 'MEDIUMTEXT',
'length' => false,
'null' => true,
'default' => null
)
);
/**
* @brief Database Table Columns
*/
protected static $sessionTableColumns = array(
array(
'name' => 'session_start',
'type' => 'INT',
'length' => 10,
'null' => false,
'default' => 0
),
array(
'name' => 'session_updated',
'type' => 'INT',
'length' => 10,
'null' => false,
'default' => 0
),
array(
'name' => 'session_status',
'type' => 'VARCHAR',
'length' => 255,
'null' => true,
'default' => 0
),
array(
'name' => 'session_current_charset',
'type' => 'VARCHAR',
'length' => 255,
'null' => true,
'default' => 0
),
array(
'name' => 'session_current_table',
'type' => 'VARCHAR',
'length' => 255,
'null' => true,
'default' => 0
),
array(
'name' => 'session_current_pkey',
'type' => 'VARCHAR',
'length' => 255,
'null' => true,
'default' => 0
),
array(
'name' => 'session_current_row',
'type' => 'VARCHAR',
'length' => 255,
'null' => true,
'default' => 0
),
array(
'name' => 'session_completed_json',
'type' => 'MEDIUMTEXT',
'length' => false,
'null' => true,
'default' => 0
),
array(
'name' => 'session_json',
'type' => 'MEDIUMTEXT',
'length' => false,
'null' => true,
'default' => 0
),
array(
'name' => 'session_has_archive',
'type' => 'INT',
'length' => 1,
'null' => true,
'default' => 0
),
array(
'name' => 'session_processing_archive',
'type' => 'INT',
'length' => 1,
'null' => true,
'default' => 0
),
array(
'name' => 'session_is_ipb',
'type' => 'INT',
'length' => 1,
'null' => true,
'default' => 0
),
);
/**
* @brief Data Store
*/
protected $_data = NULL;
/**
* @brief Changed Columns
*/
public $changed = array();
/**
* Get instance
*
* @return Output
*/
public static function i()
{
if ( self::$instance === NULL )
{
self::$instance = new self();
self::$instance->init();
}
return self::$instance;
}
/**
* Init
*
* @return void
*/
public function init()
{
/* Check to see if the session table exists */
if ( file_exists( ROOT_PATH . '/conf_global.php' ) )
{
require( ROOT_PATH . '/conf_global.php' );
}
$setDefault = false;
if ( ! \IPSUtf8\Db::i('utf8')->checkForTable( 'convert_session_tables' ) )
{
\IPSUtf8\Db::i('utf8')->createTable( array(
'name' => 'convert_session_tables',
'columns' => static::$tableTableColumns
) );
$setDefault = true;
}
if ( ! \IPSUtf8\Db::i('utf8')->checkForTable( 'convert_session' ) )
{
\IPSUtf8\Db::i('utf8')->createTable( array(
'name' => 'convert_session',
'columns' => static::$sessionTableColumns
) );
$setDefault = true;
}
else
{
try
{
$this->_data = \IPSUtf8\Db::i('utf8')->select( '*', 'convert_session' )->first();
if ( ! isset( $this->_data['session_json'] ) )
{
$setDefault = true;
}
}
catch( \UnderflowException $ex )
{
$setDefault = true;
}
}
if ( $setDefault === true )
{
$this->reset();
}
/* Populate tables */
foreach( \IPSUtf8\Db::i('utf8')->select( '*', 'convert_session_tables' ) as $row )
{
$this->tables[ $row['table_name'] ] = json_decode( $row['table_schema'], true );
}
}
/**
* Reset the session and conversion process
*/
public function reset()
{
$charSet = null;
$ipbVersion = null;
/* Is this an IPB? */
if ( \IPSUtf8\Db::i()->checkForTable('core_applications' ) )
{
try
{
$row = \IPSUtf8\Db::i()->select( '*', 'core_applications', array( 'app_directory=?', 'core' ) )->first();
if ( ! empty( $row['app_directory'] ) )
{
$ipbVersion = $row['app_long_version'];
}
}
catch( \UnderflowException $ex ) { }
}
if ( !SOURCE_CHARSET_OVERRIDE )
{
if ( $ipbVersion >= 40000 )
{
/* >= 4.0 is UTF-8 */
$charSet = 'utf-8';
}
else if ( $ipbVersion !== null )
{
/* Attempt to grab current CHARSET if this is an IPB 3 */
if ( \IPSUtf8\Db::i()->checkForTable('core_sys_conf_settings' ) )
{
try
{
$row = \IPSUtf8\Db::i()->select( '*', 'core_sys_conf_settings', array( 'conf_key=?', 'gb_char_set' ) )->first();
if ( ! empty( $row['conf_key'] ) )
{
$charSet = ( $row['conf_value'] ) ? $row['conf_value'] : $row['conf_default'];
}
}
catch( \UnderflowException $ex ) { }
}
}
}
else
{
$charSet = strtolower( SOURCE_CHARSET_OVERRIDE );
}
$tableData = $this->_getTables();
\IPSUtf8\Db::i('utf8')->delete( 'convert_session_tables' );
foreach( $tableData['tables'] as $name => $schema )
{
\IPSUtf8\Db::i('utf8')->insert( 'convert_session_tables', array(
'table_name' => $name,
'table_schema' => json_encode( $schema )
) );
}
$this->_data = array(
'session_start' => time(),
'session_updated' => time(),
'session_status' => null,
'session_current_charset' => strtolower( $charSet ),
'session_current_table' => null,
'session_current_pkey' => null,
'session_current_row' => 0,
'session_completed_json' => json_encode( array() ),
'session_json' => json_encode( $tableData['data'] ),
'session_has_archive' => 0,
'session_processing_archive' => 0,
'session_is_ipb' => $ipbVersion === null ? false : true
);
/* Change database collation */
require( ROOT_PATH . '/conf_global.php' );
if ( ! empty( $INFO['archive_remote_sql_host'] ) AND ! empty( $INFO['archive_remote_sql_database'] ) AND ! empty( $INFO['archive_remote_sql_user'] ) )
{
$this->_data['session_has_archive'] = 1;
}
\IPSUtf8\Db::i('utf8')->delete( 'convert_session' );
\IPSUtf8\Db::i('utf8')->insert( 'convert_session', $this->_data );
}
/**
* Update 'all_tables'
*
* @return void
*/
public function updateTableData()
{
$tableData = $this->_getTables();
\IPSUtf8\Db::i('utf8')->delete( 'convert_session_tables' );
foreach( $tableData['tables'] as $name => $schema )
{
\IPSUtf8\Db::i('utf8')->insert( 'convert_session_tables', array(
'table_name' => $name,
'table_schema' => json_encode( $schema )
) );
}
/* Populate tables */
foreach( \IPSUtf8\Db::i('utf8')->select( '*', 'convert_session_tables' ) as $row )
{
$this->tables[ $row['table_name'] ] = json_decode( $row['table_schema'], true );
}
$this->save();
}
/**
* Grab all tables in the database that we're going to convert
*
* @return array
*/
protected function _getTables()
{
/* Grab all tables to convert */
$stmt = \IPSUtf8\Db::i()->prepare( "SHOW TABLES" );
$stmt->execute();
$stmt->bind_result( $tableName );
$tables = array();
$allTables = array();
$data = array( 'version' => \IPSUtf8\Convert::VERSION_ID, 'tableCount' => 0, 'totalCount' => 0, 'convertedCount' => 0, 'charSets' => array() );
if ( defined( 'FORCE_CONVERT' ) AND FORCE_CONVERT === TRUE )
{
$data['force_conversion'] = 1;
}
while ( $stmt->fetch() === true )
{
/* Skip x_utf_ prefixed tables in case we don't have a prefix set */
if ( mb_substr( $tableName, 0, 6 ) === 'x_utf_' )
{
continue;
}
if ( mb_substr( $tableName, 0, 5 ) === 'orig_' )
{
continue;
}
/* Skip if it's an _old or _utftemp table */
if( mb_substr( $tableName, -4 ) == '_old' )
{
continue;
}
if( mb_substr( $tableName, -8 ) == '_utftemp' )
{
continue;
}
if ( mb_substr( $tableName, 0, mb_strlen( \IPSUtf8\Db::i()->prefix ) ) === \IPSUtf8\Db::i()->prefix )
{
$tableNameNoPrefix = $tableName;
if ( \IPSUtf8\Db::i()->prefix )
{
$tableNameNoPrefix = mb_substr( $tableName, mb_strlen( \IPSUtf8\Db::i()->prefix ) );
}
$tables[] = $tableNameNoPrefix;
}
}
foreach( $tables as $table )
{
/* Get count */
$row = \IPSUtf8\Db::i()->query( "SELECT COUNT(*) as count FROM `" . \IPSUtf8\Db::i()->prefix . "{$table}`" )->fetch_assoc();
$count = $row['count'];
$data['totalCount'] += $count;
$data['tableCount']++;
$row = \IPSUtf8\Db::i()->query( "SHOW CREATE TABLE `" . \IPSUtf8\Db::i()->prefix . "{$table}`" )->fetch_assoc();
if ( preg_match( '#\scharset=([a-z0-9]+?)(\s|$)#i', $row['Create Table'], $matches ) )
{
$tblCharset = $matches[1];
$data['charSets'][ mb_strtolower( $tblCharset ) ][] = $table;
}
else
{
$row = \IPSUtf8\Db::i()->query( "SHOW TABLE STATUS WHERE Name='" . \IPSUtf8\Db::i()->prefix . "{$table}'" )->fetch_assoc();
if ( isset( $row['Collation'] ) )
{
$tblCharset = mb_substr( $row['Collation'], 0, strpos( $row['Collation'], '_' ) );
$data['charSets'][ mb_strtolower( $tblCharset ) ][] = $table;
}
else
{
$data['charSets'][ mb_strtolower( \IPSUtf8\Convert::i()->database_charset ) ][] = $table;
}
}
/* Make sure we can JSON Encode this table... MySQL Comments with UTF8 characters can cause it to fail */
$definition = \IPSUtf8\Db::i()->getTableDefinition( $table );
try
{
$test = @json_encode( $definition );
if ( json_last_error() != JSON_ERROR_NONE )
{
throw new \ErrorException;
}
}
catch( \ErrorException $e )
{
$definition = static::arrayWalkRecursive( $definition, function( $value ) { return utf8_encode( $value ); } );
/* Try it again - if it fails this time, remove the column comments as that is likely causing the issue if they have non-latin characters */
$test = @json_encode( $definition );
if ( json_last_error() != JSON_ERROR_NONE )
{
foreach( $definition['columns'] AS $column => $data )
{
if ( isset( $data['comment'] ) )
{
unset( $definition['columns'][ $column ]['comment'] );
}
}
}
}
/* If the collation is missing, let's do a table status check and see if we can figure it out */
if ( empty( $definition['collation'] ) )
{
$status = \IPSUtf8\Db::i()->query( "SHOW TABLE STATUS WHERE Name='" . \IPSUtf8\Db::i()->prefix . "{$table}'" )->fetch_assoc();
if( isset( $status['Collation'] ) )
{
$definition['collation'] = $status['Collation'];
}
}
/* Get other data */
$allTables[ $table ] = array(
'name' => $table,
'definition' => $definition,
'count' => $count,
'charset' => $tblCharset
);
}
return array( 'tables' => $allTables, 'data' => $data );
}
/**
* Save the state of the conversion
*/
public function __destruct()
{
$this->save();
}
/**
* Get value from data store
*
* @param mixed $key Key
* @return mixed Value from the datastore
*/
public function __get( $key )
{
if ( mb_substr( $key, 0, 8 ) != 'session_' )
{
$key = 'session_' . $key;
}
if( isset( $this->_data[ $key ] ) )
{
if ( mb_substr( $key, -5 ) == '_json' AND ! is_array( $this->_data[ $key ] ) )
{
$this->_data[ $key ] = json_decode( $this->_data[ $key ], true );
}
return $this->_data[ $key ];
}
return NULL;
}
/**
* Magic isset method
*
* @param string $key Key
*/
public function __isset( $key )
{
if ( mb_substr( $key, 0, 8 ) != 'session_' )
{
$key = 'session_' . $key;
}
return isset( $this->_data[ $key ] );
}
/**
* Set value in data store
*
* @param mixed $key Key
* @param mixed $value Value
* @return void
*/
public function __set( $key, $value )
{
if ( mb_substr( $key, 0, 8 ) != 'session_' )
{
$key = 'session_' . $key;
}
if( array_key_exists( $key, $this->_data ) )
{
$this->_data[ $key ] = $value;
$this->changed[ $key ] = $value;
}
}
/**
* Time taken for conversion
*
* @param bool $formatted Return the time formatted or in seconds
* @return mixed
*/
public function timeTaken( $formatted=false )
{
$seconds = ( $this->_data['session_updated'] - $this->_data['session_start'] );
if ( $formatted )
{
$s = $seconds % 60;
$m = floor( ( $seconds % 3600 ) / 60 );
$h = floor( ( $seconds % 86400 ) / 3600 );
if ( $h )
{
return "{$h} hour(s), {$m} minute(s) and {$s} seconds.";
}
else
{
return "{$m} minute(s) and {$s} seconds.";
}
}
else
{
return $seconds;
}
}
/**
* Save Changed Columns
*
* @return void
*/
public function save()
{
$this->_data['session_updated'] = time();
$insert = array( 'session_updated' => time() );
if ( ! empty( $this->changed ) )
{
/* JSON encode if required */
foreach ( array_merge( $this->_data, $this->changed ) as $k => $v )
{
$insert[ $k ] = ( mb_substr( $k, -5 ) == '_json' AND is_array( $v ) ) ? json_encode( $v, true ) : $v;
}
}
else
{
foreach ( $this->_data as $k => $v )
{
$insert[ $k ] = ( mb_substr( $k, -5 ) == '_json' AND is_array( $v ) ) ? json_encode( $v, true ) : $v;
}
}
/* Save */
if ( count( $insert ) == count( static::$sessionTableColumns ) )
{
\IPSUtf8\Db::i('utf8')->update( 'convert_session', $insert );
}
/* Reset our log of what's changed */
$this->changed = array();
}
/**
* Recursively apply a callback to an array - array_walk_recursive does not recurse into sub-sub-arrays, so we need a custom method
*
* @param array $array The array
* @param callback $callback The callback
* @return array The filtered array.
*/
public function arrayWalkRecursive( $array, $callback )
{
if ( ! is_array( $array ) )
{
trigger_error( "\$array is not an array in \IPSUtf8\Session::arrayWalkRecursive()", E_USER_ERROR );
}
foreach( $array AS $key => $value )
{
if ( is_array( $array[$key] ) )
{
$array[$key] = static::arrayWalkRecursive( $array[$key], $callback );
}
else
{
$array[$key] = $callback( $value );
}
}
return $array;
}
}