Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

@@ -84,6 +84,30 @@ class _Expressionengine extends \IPS\convert\Software
);
}
/**
* Allows software to add additional menu row options
*
* @param array $rows Existing rows
* @return array
*/
public function extraMenuRows( $rows )
{
if( isset( $rows['convertPrivateMessageReplies'] ) )
{
$rows['convertPmAttachments'] = array(
'step_method' => 'convertPmAttachments',
'step_title' => 'convert_attachments',
'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments' )->first(),
'source_rows' => $this->countRows( static::canConvert()['convertPmAttachments']['table'], static::canConvert()['convertPmAttachments']['where'] ),
'per_cycle' => 100,
'dependencies' => array( 'convertPrivateMessageReplies' ),
'link_type' => 'core_attachments',
);
}
return $rows;
}
/**
* Can we convert passwords from this software.
*
@@ -288,7 +312,7 @@ class _Expressionengine extends \IPS\convert\Software
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
}
@@ -724,4 +748,39 @@ class _Expressionengine extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$length = \strlen( $member->conv_password );
$providedHash = FALSE;
switch( $length )
{
/* MD5 */
case 32:
$providedHash = md5( $password );
break;
/* SHA1 */
case 40:
$providedHash = sha1( $password );
break;
/* SHA256 */
case 64:
$providedHash = hash( 'sha256', $password );
break;
/* SHA512 */
case 128:
$providedHash = hash( 'sha512', $password );
break;
}
return ( \IPS\Login::compareHashes( $member->conv_password, $providedHash ) ) ? TRUE : FALSE;
}
}
File diff suppressed because it is too large. Load diff
@@ -229,4 +229,32 @@ class _Joomla extends \IPS\convert\Software
$libraryClass->setLastKeyValue( $row['id'] );
}
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
/* Joomla 3 */
if( preg_match( '/^\$2[ay]\$(0[4-9]|[1-2][0-9]|3[0-1])\$[a-zA-Z0-9.\/]{53}/', $member->conv_password ) )
{
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 8, TRUE );
return $ph->CheckPassword( $password, $member->conv_password ) ? TRUE : FALSE;
}
/* Joomla 2 */
if ( \IPS\Login::compareHashes( $member->conv_password, md5( $password . $member->misc ) ) )
{
return TRUE;
}
else
{
return FALSE;
}
}
}
@@ -283,10 +283,30 @@ class _Mybb extends \IPS\convert\Software
/* First Post Data */
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_announce_rebuild" );
}
/**
* Fix post data
*
* @param string raw post data
* @return string parsed post data
*/
public static function fixPostData( $post )
{
/* Replace align tags with left/right/center/justify */
$post = preg_replace( '/\[align=(justify|left|right)\](.+?)\[\/align\]/i', '[$1]$2[/$1]', $post );
/* remove justify, since we don't support it */
$post = str_replace( array( '[justify]', '[/justify]' ), '', $post );
return $post;
}
/**
* Convert announcements
*
@@ -899,7 +919,7 @@ class _Mybb extends \IPS\convert\Software
/* Warnings */
foreach( $this->db->select( '*', 'warnings', array( "uid=? AND pid=?", $row['uid'], 0 ) ) AS $warning )
{
$libraryClass->convertWarnLog( array(
$warnId = $libraryClass->convertWarnLog( array(
'wl_id' => $warning['wid'],
'wl_member' => $warning['uid'],
'wl_moderator' => $warning['issuedby'],
@@ -909,6 +929,17 @@ class _Mybb extends \IPS\convert\Software
'wl_note_mods' => $warning['notes'],
'wl_expire_date' => $warning['expires'],
) );
/* Add a member history record for this member */
$libraryClass->convertMemberHistory( array(
'log_id' => 'w' . $warning['wid'],
'log_member' => $warning['uid'],
'log_by' => $warning['issuedby'],
'log_type' => 'warning',
'log_data' => array( 'wid' => $warnId ),
'log_date' => $warning['dateline']
)
);
}
$libraryClass->setLastKeyValue( $row['uid'] );
@@ -1078,4 +1109,23 @@ class _Mybb extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
if ( \IPS\Login::compareHashes( $member->conv_password, md5( md5( $member->misc ) . md5( $password ) ) ) )
{
return TRUE;
}
else
{
return FALSE;
}
}
}
@@ -7,7 +7,6 @@
* @package Invision Community
* @subpackage Converter
* @since 6 December 2016
* @note Only redirect scripts are supported right now
*/
namespace IPS\convert\Software\Core;
@@ -29,7 +28,7 @@ class _Photopost extends \IPS\convert\Software
public static function softwareName()
{
/* Child classes must override this method */
return "Photopost";
return "Photopost (8.x)";
}
/**
@@ -46,11 +45,242 @@ class _Photopost extends \IPS\convert\Software
/**
* Content we can convert from this software.
*
* @return NULL
* @return array
*/
public static function canConvert()
{
return NULL;
return array(
'convertGroups' => array(
'table' => 'usergroups',
'where' => NULL,
),
'convertMembers' => array(
'table' => 'users',
'where' => NULL,
)
);
}
/**
* List of conversion methods that require additional information
*
* @return array
*/
public static function checkConf()
{
return array(
'convertGroups',
'convertMembers'
);
}
/**
* Get More Information
*
* @param string $method Conversion method
* @return array
*/
public function getMoreInfo( $method )
{
$return = array();
switch( $method )
{
case 'convertGroups':
$return['convertGroups'] = array();
$options = array();
$options['none'] = 'None';
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
{
$options[$group->g_id] = $group->name;
}
foreach( $this->db->select( '*', 'usergroups' ) AS $group )
{
\IPS\Member::loggedIn()->language()->words["map_group_{$group['groupid']}"] = $group['groupname'];
\IPS\Member::loggedIn()->language()->words["map_group_{$group['groupid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
$return['convertGroups']["map_group_{$group['groupid']}"] = array(
'field_class' => 'IPS\\Helpers\\Form\\Select',
'field_default' => NULL,
'field_required' => FALSE,
'field_extra' => array( 'options' => $options ),
'field_hint' => NULL,
);
}
break;
case 'convertMembers':
$return['convertMembers'] = array();
\IPS\Member::loggedIn()->language()->words['photo_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'photo_location_nodb_desc' );
$return['convertMembers']['photo_location'] = array(
'field_class' => 'IPS\\Helpers\\Form\\Text',
'field_default' => NULL,
'field_required' => TRUE,
'field_extra' => array(),
'field_hint' => "The path to the folder where avatars are saved (no trailing slash - usually /path_to_photopost/data/avatars):",
'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
);
/* And decide what to do about these... */
foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'location', 'interests', 'occupation', 'bio' ) AS $field )
{
\IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => ucwords( $field ) ) );
\IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
$return['convertMembers']["field_{$field}"] = array(
'field_class' => 'IPS\\Helpers\\Form\\Radio',
'field_default' => 'no_convert',
'field_required' => TRUE,
'field_extra' => array(
'options' => array(
'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
),
'userSuppliedInput' => 'create_field'
),
'field_hint' => NULL
);
}
break;
}
return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
}
/**
* Finish - Adds everything it needs to the queues and clears data store
*
* @return array Message to display
*/
public function finish()
{
/* Search Index Rebuild */
\IPS\Content\Search\Index::i()->rebuild();
/* Clear Cache and Store */
\IPS\Data\Store::i()->clearAll();
\IPS\Data\Cache::i()->clearAll();
/* Non-Content Rebuilds */
\IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
/* Content Counts */
\IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_signatures_rebuild" );
}
/**
* Convert groups
*
* @return void
*/
public function convertGroups()
{
$libraryClass = $this->getLibrary();
$libraryClass::setKey( 'groupid' );
foreach( $this->fetch( 'usergroups', 'groupid' ) AS $row )
{
$info = array(
'g_id' => $row['groupid'],
'g_name' => $row['groupname']
);
$merge = $this->app->_session['more_info']['convertGroups']["map_group_{$row['groupid']}"] != 'none' ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['groupid']}"] : NULL;
$libraryClass->convertGroup( $info, $merge );
$libraryClass->setLastKeyValue( $row['groupid'] );
}
/* Now check for group promotions */
if( count( $libraryClass->groupPromotions ) )
{
foreach( $libraryClass->groupPromotions as $groupPromotion )
{
$libraryClass->convertGroupPromotion( $groupPromotion );
}
}
}
/**
* Convert members
*
* @return void
*/
public function convertMembers()
{
$libraryClass = $this->getLibrary();
$libraryClass::setKey( 'userid' );
foreach( $this->fetch( 'users', 'userid' ) AS $row )
{
$birthdayYear = $birthdayMonth = $birthdayDay = NULL;
/* Is there a birthdate? */
if( $row['birthday'] != '0000-00-00' )
{
list( $birthdayYear, $birthdayMonth, $birthdayDay ) = explode( '-', $row['birthday'] );
}
$info = array(
'member_id' => $row['userid'],
'email' => $row['email'],
'name' => $row['username'],
'md5_password' => $row['password'],
'member_group_id' => $row['usergroupid'],
'joined' => $row['joindate'] ?: NULL,
'ip_address' => $row['ipaddress'],
'last_visit' => $row['laston'],
'last_activity' => $row['laston'],
'bday_day' => $birthdayDay,
'bday_month' => $birthdayMonth,
'bday_year' => $birthdayYear,
'signature' => static::fixPostData( $row['signature'] )
);
/* Pseudo Fields */
$profileFields = array();
foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'location', 'interests', 'occupation', 'bio' ) AS $pseudo )
{
/* Are we retaining? */
if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
{
/* No, skip */
continue;
}
try
{
/* We don't actually need this, but we need to make sure the field was created */
$this->app->getLink( $pseudo, 'core_pfields_data' );
}
catch( \OutOfRangeException $e )
{
$libraryClass->convertProfileField( array(
'pf_id' => $pseudo,
'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
'pf_desc' => '',
'pf_type' => 'Text',
'pf_content' => '[]',
'pf_member_hide' => 0,
'pf_max_input' => 255,
'pf_member_edit' => 1,
'pf_show_on_reg' => 0,
'pf_admin_only' => 0,
) );
}
$profileFields[ $pseudo ] = $row[ $pseudo ];
}
$libraryClass->convertMember( $info, $profileFields, $row['avatar'], rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' ) );
$libraryClass->setLastKeyValue( $row['userid'] );
}
}
/**
@@ -28,7 +28,7 @@ class _Phpbb extends \IPS\convert\Software
public static function softwareName()
{
/* Child classes must override this method */
return "phpBB 3.1";
return "phpBB (3.1.x/3.2.x)";
}
/**
@@ -98,6 +98,24 @@ class _Phpbb extends \IPS\convert\Software
);
}
/**
* Allows software to add additional menu row options
*
* @param array $rows Existing rows
* @return array
*/
public function extraMenuRows( $rows )
{
if( isset( $rows['convertBanfilters'] ) )
{
$rows['convertBanfilters2'] = $rows['convertBanfilters'];
$rows['convertBanfilters2']['step_method'] = 'convertBanfilters2';
$rows['convertBanfilters2']['source_rows'] = $this->countRows( static::canConvert()['convertBanfilters2']['table'], static::canConvert()['convertBanfilters2']['where'] );
}
return $rows;
}
/**
* Count Source Rows for a specific step
*
@@ -310,6 +328,9 @@ class _Phpbb extends \IPS\convert\Software
/* Content Counts */
\IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
/* First Post Data */
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
@@ -698,8 +719,7 @@ class _Phpbb extends \IPS\convert\Software
{
$userfields = $this->db->select( '*', 'profile_fields_data', array( "user_id=?", $row['user_id'] ) )->first();
}
catch( \UnderflowException $e )
{}
catch( \UnderflowException $e ) {}
foreach( $this->db->select( '*', 'profile_fields' ) AS $field )
{
@@ -720,6 +740,10 @@ class _Phpbb extends \IPS\convert\Software
}
}
}
/* PM Folders */
$pmFolders = iterator_to_array( $this->db->select( 'folder_name', 'privmsgs_folder', array( 'user_id=?', $row['user_id'] ) ) );
$info['pconversation_filters'] = count( $pmFolders ) ? $pmFolders : NULL;
/* Finally */
$libraryClass->convertMember( $info, $pfields, $filename, $filepath );
@@ -946,7 +970,7 @@ class _Phpbb extends \IPS\convert\Software
$maps = array();
/* First one first initial author */
$maps[$row['author_id']] = array(
$maps[ $row['author_id'] ] = array(
'map_user_id' => $row['author_id'],
'map_is_starter' => 1,
'map_last_topic_reply' => $row['message_time'],
@@ -973,13 +997,18 @@ class _Phpbb extends \IPS\convert\Software
to a custom folder then they will have a row in privmsgs_to and we will end up resetting map_is_starter to 0 otherwise */
if( isset( $maps[ $to['user_id'] ] ) )
{
if( $to['folder_id'] > 0 )
{
$maps[ $to['user_id'] ]['map_folder_id'] = $this->_getPmFolder( $to['user_id'], $to['folder_id'] );
}
continue;
}
$maps[ $to['user_id'] ] = array(
'map_user_id' => $to['user_id'],
'map_is_starter' => 0,
'map_last_topic_reply' => $row['message_time']
'map_last_topic_reply' => $row['message_time'],
'map_folder_id' => ( $to['folder_id'] > 0 ) ? $this->_getPmFolder( $to['user_id'], $to['folder_id'] ) : FALSE
);
}
@@ -1045,4 +1074,108 @@ class _Phpbb extends \IPS\convert\Software
return NULL;
}
/**
* @brief Lookup cache
*/
protected static $pmFolders = array();
/**
* @brief PhpBB Source Folders
*/
protected static $phpBbFolders = NULL;
/**
* Get array key for the folder
*
* @param int $user User ID
* @param string $folderId Folder Name
* @return boolean
*/
protected function _getPmFolder( $user, $folderId )
{
/* Check for cached folder info */
if( isset( static::$pmFolders[ $user ][ $folderId ] ) )
{
return static::$pmFolders[ $user ][ $folderId ];
}
/* Check for cached phpBB data */
if( static::$phpBbFolders === NULL )
{
static::$phpBbFolders = iterator_to_array( $this->db->select( 'folder_id,folder_name', 'privmsgs_folder' )->setKeyField( 'folder_id' )->setValueField( 'folder_name' ) );
}
try
{
/* Does the referenced folder exist in PhpBB? */
if( !isset( static::$phpBbFolders[ $folderId ] ) )
{
throw new \OutOfRangeException;
}
$member = \IPS\Member::load( $this->app->getLink( (int) $user, 'core_members' ) );
$filters = json_decode( $member->pconversation_filters, TRUE );
/* No PM Folders or it's a guest */
if( $filters === NULL OR !$member->member_id )
{
throw new \OutOfRangeException;
}
}
catch( \OutOfRangeException $e )
{
return static::$pmFolders[ $user ][ $folderId ] = FALSE;
}
/* Search PM folders and return match */
return static::$pmFolders[ $user ][ $folderId ] = array_search( static::$phpBbFolders[ $folderId ], $filters );
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$password = html_entity_decode( $password );
$success = FALSE;
$hash = $member->conv_password;
/* phpBB 3.1 */
if( preg_match( '/^\$2[ay]\$(0[4-9]|[1-2][0-9]|3[0-1])\$[a-zA-Z0-9.\/]{53}/', $hash ) )
{
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 8, TRUE );
$success = $ph->CheckPassword( $password, $member->conv_password ) ? TRUE : FALSE;
}
$hashLibrary = new \IPS\convert\Login\HashCryptPrivate;
if ( $success === FALSE )
{
/* phpBB3 */
$single_md5_pass = md5( $password );
$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
if ( \strlen( $hash ) == 34 )
{
$success = ( \IPS\Login::compareHashes( $hash, $hashLibrary->hashCryptPrivate( $password, $hash, $itoa64 ) ) ) ? TRUE : FALSE;
}
else
{
$success = ( \IPS\Login::compareHashes( $hash, $single_md5_pass ) ) ? TRUE : FALSE;
}
}
/* phpBB2 */
if ( !$success )
{
$success = ( \IPS\Login::compareHashes( $hash, $hashLibrary->hashCryptPrivate( $single_md5_pass, $hash, $itoa64 ) ) ) ? TRUE : FALSE ;
}
return $success;
}
}
@@ -28,7 +28,7 @@ class _Punbb extends \IPS\convert\Software
public static function softwareName()
{
/* Child classes must override this method */
return "PunBB 1.x";
return "PunBB (1.x)";
}
/**
@@ -454,4 +454,35 @@ class _Punbb extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$success = FALSE;
if ( mb_strlen( $member->conv_password ) == 40 )
{
/* Password with salt */
$success = \IPS\Login::compareHashes( $member->conv_password, sha1( $member->conv_password_extra . sha1( $password ) ) );
if ( !$success )
{
/* No salt */
$success = \IPS\Login::compareHashes( $member->conv_password, sha1( $password ) );
}
}
else
{
/* MD5 */
$success = \IPS\Login::compareHashes( $member->conv_password, md5( $password ) );
}
return $success;
}
}
@@ -28,7 +28,7 @@ class _Smf extends \IPS\convert\Software
public static function softwareName()
{
/* Child classes must override this method */
return "Simple Machines Forum";
return "Simple Machines Forum (2.0.x)";
}
/**
@@ -226,7 +226,7 @@ class _Smf extends \IPS\convert\Software
'field_class' => 'IPS\\Helpers\\Form\\Radio',
'field_default' => 'user_name',
'field_required' => TRUE,
'field_extra' => array( 'options' => array( 'user_name', 'display_name' ) ),
'field_extra' => array( 'options' => array( 'user_name' => 'user_name', 'display_name' => 'display_name' ) ),
'field_hint' => NULL,
);
@@ -298,7 +298,7 @@ class _Smf extends \IPS\convert\Software
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
}
@@ -318,7 +318,7 @@ class _Smf extends \IPS\convert\Software
$post = str_replace( $search, $replace, $post );
// And img tags
$post = preg_replace("#\[img width=(\d+)(?: height=(\d+))?\]([a-zA-Z0-9.:\-/%\?&_]+)\[\/img\]#i", "[img]$3[/img]", $post);
$post = preg_replace("#\[img width=(\d+)(?: height=(\d+))?\]([a-zA-Z0-9.:\-/%\?&_=~\#\(\);:]+)\[\/img\]#i", "[img]$3[/img]", $post);
//Quotes
$post = preg_replace("#\[quote author=(.+?) link=(.+?)=(\d+)\.msg(\d+)(.+?)date=(\d+)\](.+)\[\/quote\]#i", "[quote name=\"$1\" post=\"$4\" timestamp=\"$6\"]$7[/quote]", $post);
@@ -648,7 +648,7 @@ class _Smf extends \IPS\convert\Software
'email' => $row['email_address'],
'name' => $name,
'password' => $row['passwd'],
'password_extra' => $row['password_salt'],
'password_extra' => $row['member_name'], // We need to keep the original member name for the SMF password hash
'member_group_id' => $row['id_group'],
'joined' => $row['date_registered'],
'ip_address' => $row['member_ip'],
@@ -900,4 +900,45 @@ class _Smf extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
if ( \IPS\Login::compareHashes( $member->conv_password, sha1( mb_strtolower( $member->name ) . html_entity_decode( $password ) ) ) )
{
return TRUE;
}
else if ( \IPS\Login::compareHashes( $member->conv_password, sha1( mb_strtolower( $member->name ) . $password ) ) )
{
return TRUE;
}
/* In 4.2.6 we save the original members name as the salt so that we don't have one that has been modified in the conversion process */
else if ( \IPS\Login::compareHashes( $member->conv_password, sha1( mb_strtolower( $member->conv_password_extra ) . $password ) ) )
{
return TRUE;
}
else
{
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 8, TRUE );
if( $ph->CheckPassword( mb_strtolower( $member->name ) . $password, $member->conv_password ) )
{
return TRUE;
}
/* In 4.2.6 we save the original members name as the salt so that we don't have one that has been modified in the conversion process */
else if ( $ph->CheckPassword( mb_strtolower( $member->conv_password_extra ) . $password, $member->conv_password ) )
{
return TRUE;
}
}
return FALSE;
}
}
@@ -132,6 +132,32 @@ class _UBBthreads extends \IPS\convert\Software
),
);
}
/**
* Allows software to add additional menu row options
*
* @param array $rows Existing rows
* @return array
*/
public function extraMenuRows( $rows )
{
$count = $this->countRows( static::canConvert()['convertMembersFollowers']['table'], static::canConvert()['convertMembersFollowers']['where'] );
if( $count )
{
$rows['convertMembersFollowers'] = array(
'step_method' => 'convertMembersFollowers',
'step_title' => 'convert_follows',
'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_follow', array( 'follow_app=? and follow_area=?', 'core', 'member' ) )->first(),
'source_rows' => $count,
'per_cycle' => 500,
'dependencies' => array( 'convertMembers' ),
'link_type' => 'core_follow',
);
}
return $rows;
}
/**
* Can we convert passwords from this software.
@@ -265,6 +291,15 @@ class _UBBthreads extends \IPS\convert\Software
'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'username_hint' ),
);
$return['convertMembers']['photo_location'] = array(
'field_class' => 'IPS\\Helpers\\Form\\Text',
'field_default' => NULL,
'field_required' => TRUE,
'field_extra' => array(),
'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ubbthreads_photo_path'),
'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
);
foreach( array( 'homepage', 'occupation', 'hobbies', 'location', 'icq', 'yahoo', 'aim', 'msn' ) AS $field )
{
\IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
@@ -315,7 +350,7 @@ class _UBBthreads extends \IPS\convert\Software
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
}
@@ -584,8 +619,16 @@ class _UBBthreads extends \IPS\convert\Software
/* Profile photo */
$profilePhotoName = NULL;
$profilePhotoData = NULL;
$profilePhotoPath = NULL;
if ( !empty( $row['USER_AVATAR'] ) AND ( $row['USER_AVATAR'] != 'http://' ) )
/* Is it a file? */
if( !empty( $row['USER_AVATAR'] ) AND mb_substr( $row['USER_AVATAR'], 0, 1 ) == '/' )
{
$profilePhotoPath = pathinfo( rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' ) . $row['USER_AVATAR'], PATHINFO_DIRNAME );
$profilePhotoName = pathinfo( rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' ) . $row['USER_AVATAR'], PATHINFO_BASENAME );
}
/* Or an URL? */
elseif ( !empty( $row['USER_AVATAR'] ) AND ( $row['USER_AVATAR'] != 'http://' ) AND mb_substr( $row['USER_AVATAR'], 0, 4 ) )
{
try
{
@@ -595,7 +638,7 @@ class _UBBthreads extends \IPS\convert\Software
catch( \IPS\Http\Request\Exception $e ) { }
}
$libraryClass->convertMember( $info, $pfields, $profilePhotoName, NULL, $profilePhotoData );
$libraryClass->convertMember( $info, $pfields, $profilePhotoName, $profilePhotoPath, $profilePhotoData );
$libraryClass->setLastKeyValue( $row['USER_ID'] );
}
}
@@ -609,7 +652,7 @@ class _UBBthreads extends \IPS\convert\Software
{
$libraryClass = $this->getLibrary();
foreach ( $this->fetch( 'WATCH_LISTS', 'WATCH_ID', array( 'WATCH_TYPE=?', 'u' ), 'USER_ID ASC, WATCH_ID ASC' ) as $row )
foreach ( $this->fetch( 'WATCH_LISTS', 'WATCH_ID', array( 'WATCH_TYPE=?', 'u' ) ) as $row )
{
$libraryClass->convertFollow( array(
'follow_app' => 'core',
@@ -769,7 +812,7 @@ class _UBBthreads extends \IPS\convert\Software
$data = $this->app->getLink( (int) $matches[1], array( 'members', 'core_members' ) );
$item = \IPS\Member::load( $data );
if( $item->canView() )
if( \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
{
return $item->url();
}
@@ -782,4 +825,37 @@ class _UBBthreads extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$hash = $member->members_pass_hash;
$salt = $member->members_pass_salt;
if ( \IPS\Login::compareHashes( $hash, md5( $password ) ) )
{
return TRUE;
}
// Not using md5, UBB salts the password with the password
// IPB already md5'd it though, *sigh*
if ( \IPS\Login::compareHashes( $hash, md5( md5( $salt ) . crypt( $password, $password ) ) ) )
{
return TRUE;
}
// Now standard IPB check.
if ( \IPS\Login::compareHashes( $hash, md5( md5( $salt ) . md5( $password ) ) ) )
{
return TRUE;
}
return FALSE;
}
}
@@ -27,7 +27,7 @@ class _Vanilla extends \IPS\convert\Software
public static function softwareName()
{
/* Child classes must override this method */
return "Vanilla 2";
return "Vanilla 2 (2.2.x/2.3.x)";
}
/**
@@ -118,8 +118,8 @@ class _Vanilla extends \IPS\convert\Software
* Resolve the filesystem location of a file from a path stored in the database
* There are several different ways Vanilla may store these, including as remote URL's
*
* @param $location File location retrieved from the database
* @param $uploadsPath Configured uploads path
* @param string $location File location retrieved from the database
* @param string $uploadsPath Configured uploads path
* @return \IPS\Http\Url|string|void
*/
public static function parseMediaLocation( $location, $uploadsPath )
@@ -325,7 +325,7 @@ class _Vanilla extends \IPS\convert\Software
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
}
@@ -566,4 +566,31 @@ class _Vanilla extends \IPS\convert\Software
$libraryClass->setLastKeyValue( $row['MessageID'] );
}
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
/* Vanilla 2.2 */
if( preg_match( '/^\$2[ay]\$(0[4-9]|[1-2][0-9]|3[0-1])\$[a-zA-Z0-9.\/]{53}/', $member->conv_password ) OR mb_substr( $member->conv_password, 0, 3 ) == '$P$' )
{
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 8, TRUE );
return $ph->CheckPassword( $password, $member->conv_password ) ? TRUE : FALSE;
}
if ( \IPS\Login::compareHashes( $member->conv_password, md5( md5( str_replace( ''', "'", html_entity_decode( $password ) ) ) . $member->misc ) ) )
{
return TRUE;
}
else
{
return FALSE;
}
}
}
@@ -166,6 +166,43 @@ class _Vbulletin extends \IPS\convert\Software
)
);
}
/**
* Count Source Rows for a specific step
*
* @param string $table The table containing the rows to count.
* @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
* @param bool $recache Skip cache and pull directly (updating cache)
* @return integer
* @throws \IPS\convert\Exception
*/
public function countRows( $table, $where=NULL, $recache=FALSE )
{
switch( $table )
{
case 'userchangelog':
return $this->db->select( 'count(changeid)', 'userchangelog', array( $this->db->in( 'fieldname', array_keys( static::$changeLogTypes ) ) ) )->first();
break;
case 'pm':
/* vB doesn't put an index on a column that makes this converison much quicker, so we'll do it. */
if( !$this->db->checkForIndex( 'pm', 'parent_id' ) )
{
$this->db->addIndex( 'pm', array(
'type' => 'key',
'name' => 'parent_id',
'columns' => array( 'parentpmid', 'folderid' )
) );
}
return parent::countRows( $table, $where, $recache );
break;
default:
return parent::countRows( $table, $where, $recache );
break;
}
}
/**
* Can we convert passwords from this software.
@@ -479,12 +516,15 @@ class _Vbulletin extends \IPS\convert\Software
/* Content Counts */
\IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
/* Clubs */
\IPS\Task::queue( 'convert', 'RecountClubMembers', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* First Post Data */
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_announce_rebuild" );
}
@@ -882,7 +922,7 @@ class _Vbulletin extends \IPS\convert\Software
/**
* @brief Member history type map
*/
protected $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'usergroupid' => 'member_group_id', 'membergroupids' => 'mgroup_others' );
protected static $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'usergroupid' => 'member_group_id', 'membergroupids' => 'mgroup_others' );
/**
* Convert member history
@@ -892,50 +932,15 @@ class _Vbulletin extends \IPS\convert\Software
public function convertMemberHistory()
{
$libraryClass = $this->getLibrary();
$libraryClass::setKey( 'changeid' );
$fixGroups = function( $string )
foreach( $this->fetch( 'userchangelog', 'changeid', array( $this->db->in( 'fieldname', array_keys( static::$changeLogTypes ) ) ) ) AS $change )
{
$groups = explode( ',', $string );
$newGroups = array();
array_walk( $groups, function( &$item, $key ) use ( &$newGroups )
{
try
{
$item = $this->app->getLink( $item, 'core_groups' );
$newGroups[] = $item;
}
catch( \OutOfRangeException $e ){}
});
return implode( ',', $newGroups );
};
foreach( $this->fetch( 'userchangelog', 'changeid' ) AS $change )
{
if( !in_array( $change['fieldname'], array_keys( $this->changeLogTypes ) ) )
{
$libraryClass->setLastKeyValue( $change['changeid'] );
continue;
}
/* Any processing we need to do */
switch( $this->changeLogTypes[ $change['fieldname'] ] )
{
case 'member_group_id':
case 'mgroup_others':
$change['oldvalue'] = $fixGroups( $change['oldvalue'] );
$change['newvalue'] = $fixGroups( $change['newvalue'] );
break;
}
$libraryClass->convertMemberHistory( array(
'log_id' => $change['changeid'],
'log_member' => $change['userid'],
'log_by' => $change['adminid'],
'log_type' => $this->changeLogTypes[ $change['fieldname'] ],
'log_type' => static::$changeLogTypes[ $change['fieldname'] ],
'log_data' => array( 'old' => $change['oldvalue'], 'new' => $change['newvalue'] ),
'log_date' => $change['change_time']
)
@@ -1241,7 +1246,7 @@ class _Vbulletin extends \IPS\convert\Software
}
}
$libraryClass->convertMember( $info, $profileFields, $filename, $filepath, $filedata );
$libraryClass->convertMember( $info, $profileFields, $filename, $filepath );
/* Any friends need converting to followers? */
foreach( $this->db->select( '*', 'userlist', array( "type=? AND friend=? AND userid=?", 'buddy', 'yes', $user['userid'] ) ) AS $follower )
@@ -1933,10 +1938,11 @@ class _Vbulletin extends \IPS\convert\Software
}
/* Profile URLs can be in one of 3 formats really...
* /member.php/1-name
* /members/1-name
* /member.php?u=1
*/
* /member.php/1-name
* /members/1-name
* /member.php?u=1
* /member.php?1-name
*/
$path = $url->data[ \IPS\Http\Url::COMPONENT_PATH ];
if( mb_strpos( $path, 'member.php' ) !== FALSE )
{
@@ -1944,6 +1950,10 @@ class _Vbulletin extends \IPS\convert\Software
{
$oldId = \IPS\Request::i()->u;
}
elseif( preg_match( '#^(\d+)-[^/]+#i', $url->data[ \IPS\Http\Url::COMPONENT_QUERY ], $matches ) )
{
$oldId = $matches[1];
}
else
{
$queryStringPieces = explode( '-', mb_substr( $path, mb_strpos( $path, 'member.php/' ) + mb_strlen( 'member.php/' ) ) );
@@ -1971,4 +1981,23 @@ class _Vbulletin extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public static function login( $member, $password )
{
if ( \IPS\Login::compareHashes( $member->conv_password, md5( md5( str_replace( ''', "'", html_entity_decode( $password ) ) ) . $member->conv_password_extra ) ) )
{
return TRUE;
}
else
{
return FALSE;
}
}
}
@@ -158,6 +158,10 @@ class _Vbulletin5 extends \IPS\convert\Software
$where = array( $this->db->in( 'nodeid', $clubs ) );
return parent::countRows( $table, $where, $recache );
break;
case 'userchangelog':
return $this->db->select( 'count(changeid)', 'userchangelog', array( $this->db->in( 'fieldname', array_keys( static::$changeLogTypes ) ) ) )->first();
break;
default:
return parent::countRows( $table, $where, $recache );
@@ -464,8 +468,14 @@ class _Vbulletin5 extends \IPS\convert\Software
/* Content Counts */
\IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
/* Clubs */
\IPS\Task::queue( 'convert', 'RecountClubMembers', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* First Post Data */
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild" );
}
@@ -488,6 +498,77 @@ class _Vbulletin5 extends \IPS\convert\Software
/* Remove video tags and allow our parser to handle the embeds it supports */
$post = preg_replace( "#\[video=[a-z]+;[a-z0-9_]+\](.*?)\[\/video\]#i", '$1', $post );
/* IMG2 BBcode */
$matches = array();
preg_match_all( "/\[IMG2=JSON](.+?)\[\/IMG2\]/i", $post, $matches );
if ( count( $matches ) )
{
foreach( $matches[0] as $k => $v )
{
try
{
/* Make sure the JSON value exists */
if( !isset( $matches[1][ $k ] ) )
{
throw new \DomainException;
}
$decoded = json_decode( $matches[1][ $k ], TRUE );
/* Make sure the JSON is valid and contains src */
if( !isset( $decoded['src'] ) OR empty( $decoded['src'] ) )
{
throw new \DomainException;
}
$post = str_replace( $v, "[img]{$decoded['src']}[/img]", $post );
}
catch( \DomainException $e )
{
continue;
}
}
}
/* Mentions */
$matches = array();
preg_match_all( '#\[user(=\"?(\d+)\"?)?\](.+?)\[\/user\]#i', $post, $matches ); // this purposefully matches more than the default vB mention.
if ( count( $matches ) )
{
if ( isset( $matches[1] ) )
{
$mentions = array();
foreach( $matches[1] AS $k => $v )
{
if ( isset( $matches[3][$k] ) )
{
$name = trim( $matches[3][ $k ], '@' );
$mentions[ $name ] = $v;
}
}
$maps = array();
$urls = array();
$cardUrls = array();
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'name', array_keys( $mentions ) ) ) ), 'IPS\Member' ) AS $member )
{
$maps[ $member->name ] = $member->member_id;
$urls[ $member->name ] = $member->url();
$cardUrls[ $member->name ] = $member->url()->setQueryString( 'do', 'hovercard' );
}
foreach( $mentions AS $member_name => $member_id )
{
$maps[ $member_name ] = preg_quote( $maps[ $member_name ], '#' );
$memberNameEscaped = preg_quote( $member_name, '#' );
$post = preg_replace( "#\[user(=\"?(\d+)\"?)?\]@?{$memberNameEscaped}\[\/user\]#i", "<a contenteditable=\"false\" rel=\"\" href=\"{$urls[ $member_name ]}\" data-mentionid=\"{$maps[ $member_name ]}\" data-ipshover-target=\"{$cardUrls[ $member_name ]}\" data-ipshover=\"\">@{$member_name}</a>", $post );
}
}
}
return $post;
}
@@ -822,7 +903,7 @@ class _Vbulletin5 extends \IPS\convert\Software
/**
* @brief Member history type map
*/
protected $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'usergroupid' => 'member_group_id', 'membergroupids' => 'mgroup_others' );
protected static $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'usergroupid' => 'member_group_id', 'membergroupids' => 'mgroup_others' );
/**
* Convert member history
@@ -832,53 +913,18 @@ class _Vbulletin5 extends \IPS\convert\Software
public function convertMemberHistory()
{
$libraryClass = $this->getLibrary();
$libraryClass::setKey( 'changeid' );
$fixGroups = function( $string )
foreach( $this->fetch( 'userchangelog', 'changeid', array( $this->db->in( 'fieldname', array_keys( static::$changeLogTypes ) ) ) ) AS $change )
{
$groups = explode( ',', $string );
$newGroups = array();
array_walk( $groups, function( &$item, $key ) use ( &$newGroups )
{
try
{
$item = $this->app->getLink( $item, 'core_groups' );
$newGroups[] = $item;
}
catch( \OutOfRangeException $e ){}
});
return implode( ',', $newGroups );
};
foreach( $this->fetch( 'userchangelog', 'changeid' ) AS $change )
{
if( !in_array( $change['fieldname'], array_keys( $this->changeLogTypes ) ) )
{
$libraryClass->setLastKeyValue( $change['changeid'] );
continue;
}
/* Any processing we need to do */
switch( $this->changeLogTypes[ $change['fieldname'] ] )
{
case 'member_group_id':
case 'mgroup_others':
$change['oldvalue'] = $fixGroups( $change['oldvalue'] );
$change['newvalue'] = $fixGroups( $change['newvalue'] );
break;
}
$libraryClass->convertMemberHistory( array(
'log_id' => $change['changeid'],
'log_member' => $change['userid'],
'log_by' => $change['adminid'],
'log_type' => $this->changeLogTypes[ $change['fieldname'] ],
'log_type' => static::$changeLogTypes[ $change['fieldname'] ],
'log_data' => array( 'old' => $change['oldvalue'], 'new' => $change['newvalue'] ),
'log_date' => $change['change_time'],
'log_ip_address' => long2ip( $change['ipaddress'] )
'log_ip_address' => long2ip( (int) $change['ipaddress'] )
)
);
@@ -1221,7 +1267,7 @@ class _Vbulletin5 extends \IPS\convert\Software
$libraryClass->convertMemberHistory( array(
'log_id' => 'w' . $warn['nodeid'],
'log_member' => $warn['infracteduserid'],
'log_by' => $warn['userid'],
'log_by' => $warn['actionuserid'],
'log_type' => 'warning',
'log_data' => array( 'wid' => $warnId ),
'log_date' => $warn['actiondateline']
@@ -1344,18 +1390,19 @@ class _Vbulletin5 extends \IPS\convert\Software
'map_user_active' => 1,
'map_user_banned' => 0,
'map_has_unread' => 0,
'map_is_starter' => ( $pm['userid'] == $pm['userid'] ) ? 1 : 0,
'map_is_starter' => 1,
);
foreach( $this->db->select( '*', 'node', array( "parentid=?", $pm['nodeid'] ) ) AS $post )
/* All other maps */
foreach( $this->db->select( '*', 'sentto', array( "nodeid=? AND userid!=?", $pm['nodeid'], $pm['userid'] ) ) AS $map )
{
$maps[ $post['userid'] ] = array(
'map_user_id' => $post['userid'],
$maps[ $map['userid'] ] = array(
'map_user_id' => $map['userid'],
'map_topic_id' => $pm['nodeid'],
'map_user_active' => 1,
'map_user_active' => !$map['deleted'],
'map_user_banned' => 0,
'map_has_unread' => 0,
'map_is_starter' => ( $post['userid'] == $pm['userid'] ) ? 1 : 0,
'map_has_unread' => !$map['msgread'],
'map_is_starter' => 0
);
}
@@ -1880,4 +1927,42 @@ class _Vbulletin5 extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public static function login( $member, $password )
{
/* If no algorithm is defined, use the legacy handler */
if ( \strpos( $member->misc, 'blowfish' ) === FALSE and \strpos( $member->misc, 'legacy' ) === FALSE )
{
return \IPS\convert\Software\Core\Vbulletin::login( $member, $password );
}
/* Which do we need to use? */
$algo = explode( ':', $member->misc );
switch( $algo[ 0 ] )
{
case 'blowfish':
/* vBulletin uses PasswordHash, however they md5 once the password prior to checking */
$md5_once_password = md5( $password );
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( $algo[ 1 ], FALSE );
return $ph->CheckPassword( $md5_once_password, $member->conv_password );
break;
case 'legacy':
/* Legacy Passwords are stored in a format of HASH SALT so we need to explode on the space. */
$token = explode( ' ', $member->conv_password );
$member->conv_password = $token[0];
$member->conv_password_extra = $token[1];
return \IPS\convert\Software\Core\Vbulletin::login( $member, $password );
break;
}
}
}
@@ -231,4 +231,34 @@ class _Wordpress extends \IPS\convert\Software
return array( "f_search_index_rebuild", "f_clear_caches" );
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$success = FALSE;
// If the hash is still md5...
if ( \strlen( $member->conv_password ) <= 32 )
{
$success = ( \IPS\Login::compareHashes( $member->conv_password, md5( $password ) ) ) ? TRUE : FALSE;
}
// New pass hash check
else
{
// Init the pass class
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 8, TRUE );
// Check it
$success = $ph->CheckPassword( $password, $member->conv_password ) ? TRUE : FALSE;
}
return $success;
}
}
@@ -1,7 +1,7 @@
<?php
/**
* @brief Converter XenForo 1.x Master Class
* @brief Converter XenForo 1.x/2.x Master Class
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @package Invision Community
@@ -20,6 +20,48 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _Xenforo extends \IPS\convert\Software
{
/**
* @brief The similarities between XF1 and XF2 are close enough that we can use the same converter
*/
public static $isLegacy = NULL;
/**
* Constructor
*
* @param \IPS\convert\App The application to reference for database and other information.
* @param bool Establish a DB connection
* @return void
* @throws \InvalidArgumentException
*/
public function __construct( \IPS\convert\App $app, $needDB=TRUE )
{
$return = parent::__construct( $app, $needDB );
if ( $needDB )
{
try
{
/* Is this XF1 or XF2 */
if ( static::$isLegacy === NULL )
{
$version = $this->db->select( 'MAX(version_id)', 'xf_template', array( "addon_id=?", 'XF' ) )->first();
if ( $version < 2000010 )
{
static::$isLegacy = TRUE;
}
else
{
static::$isLegacy = FALSE;
}
}
}
catch( \Exception $e ) {} # If we can't query, we won't be able to do anything anyway
}
return $return;
}
/**
* Software Name
*
@@ -27,7 +69,7 @@ class _Xenforo extends \IPS\convert\Software
*/
public static function softwareName()
{
return "XenForo (1.x)";
return "XenForo (1.5.x/2.0.x)";
}
/**
@@ -85,10 +127,10 @@ class _Xenforo extends \IPS\convert\Software
'where' => NULL
),
'convertMemberHistory' => array(
'table' => 'xf_user_change_log',
'table' => ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) ? 'xf_change_log' : 'xf_user_change_log',
'where' => NULL
),
'convertWarnReason' => array(
'convertWarnReasons' => array(
'table' => 'xf_warning_definition',
'where' => NULL
),
@@ -123,6 +165,34 @@ class _Xenforo extends \IPS\convert\Software
);
}
/**
* Count Source Rows for a specific step
*
* @param string $table The table containing the rows to count.
* @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
* @param bool $recache Skip cache and pull directly (updating cache)
* @return integer
* @throws \IPS\convert\Exception
*/
public function countRows( $table, $where=NULL, $recache=FALSE )
{
switch( $table )
{
/* XF1 */
case 'xf_user_change_log':
return $this->db->select( 'count(log_id)', 'xf_user_change_log', array( $this->db->in( 'field', array_keys( static::$changeLogTypes ) ) ) )->first();
break;
/* XF2 */
case 'xf_change_log':
return $this->db->select( 'count(log_id)', 'xf_change_log', array( 'content_type=? AND ' . $this->db->in( 'field', array_keys( static::$changeLogTypes ) ), 'user' ) )->first();
break;
default:
return parent::countRows( $table, $where, $recache );
break;
}
}
/**
* Can we convert passwords from this software.
*
@@ -218,7 +288,7 @@ class _Xenforo extends \IPS\convert\Software
foreach( $this->db->select( '*', 'xf_user_field' ) AS $field )
{
\IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}"] = $this->getPhrase( "user_field_{$field['field_id']}" );
\IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}"] = $this->getPhrase( "user_field_{$field['field_id']}", "user_field_title.{$field['field_id']}" );
\IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
$return['convertProfileFields']["map_pfield_{$field['field_id']}"] = array(
@@ -324,7 +394,7 @@ class _Xenforo extends \IPS\convert\Software
continue;
}
$title = $this->getPhrase( 'option_' . $setting );
$title = $this->getPhrase( 'option_' . $setting, 'option.' . $setting );
if ( !$title )
{
@@ -340,13 +410,15 @@ class _Xenforo extends \IPS\convert\Software
/**
* Helper to fetch a xenforo phrase
*
* @param string $title Phrase
* @param string $xfOneTitle XF1 Phrase title
* @param string $xfTwoTitle XF2 Phrase Title
* @return string|null
*/
protected function getPhrase( $title )
protected function getPhrase( $xfOneTitle, $xfTwoTitle )
{
try
{
$title = ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) ? $xfTwoTitle : $xfOneTitle;
return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
}
catch( \UnderflowException $e )
@@ -379,6 +451,9 @@ class _Xenforo extends \IPS\convert\Software
/* First Post Data */
\IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
/* Attachments */
\IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 1, array( 'app' ) );
/* Rebuild Leaderboard */
\IPS\Task::queue( 'core', 'RebuildReputationLeaderboard', array(), 4 );
\IPS\Db::i()->delete('core_reputation_leaderboard_history');
@@ -395,7 +470,7 @@ class _Xenforo extends \IPS\convert\Software
public static function fixPostData( $post )
{
// run everything through htmlspecialchars to prevent XSS
$post = htmlspecialchars( $post, \IPS\HTMLENTITIES | ENT_QUOTES, 'UTF-8', FALSE );
$post = htmlspecialchars( $post, ENT_DISALLOWED | ENT_QUOTES, 'UTF-8', FALSE );
// find YouTube ID's and replace.
$post = preg_replace( '#\[media=youtube\](.+?)\[/media\]#i', '[media]http://www.youtube.com/watch?v=$1[/media]', $post );
@@ -410,9 +485,9 @@ class _Xenforo extends \IPS\convert\Software
$mentions = array();
foreach( $matches[1] AS $k => $v )
{
if ( isset( $matches[2][$k] ) )
if ( isset( $matches[2][ $k ] ) )
{
$name = trim( $matches[2][$k], '@' );
$name = trim( $matches[2][ $k ], '@' );
$mentions[$name] = $v;
}
}
@@ -422,17 +497,17 @@ class _Xenforo extends \IPS\convert\Software
$cardUrls = array();
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'name', array_keys( $mentions ) ) ) ), 'IPS\Member' ) AS $member )
{
$maps[$member->name] = $member->member_id;
$urls[$member->name] = $member->url();
$cardUrls[$member->name] = $member->url()->setQueryString( 'do', 'hovercard' );
$maps[ $member->name ] = $member->member_id;
$urls[ $member->name ] = $member->url();
$cardUrls[ $member->name ] = $member->url()->setQueryString( 'do', 'hovercard' );
}
foreach( $mentions AS $member_name => $member_id )
{
$maps[$member_name] = preg_quote( $maps[$member_name], '#' );
$member_name = preg_quote( $member_name, '#' );
$maps[ $member_name ] = preg_quote( $maps[ $member_name ], '#' );
$memberNameEscaped = preg_quote( $member_name, '#' );
$post = preg_replace( "#\[user={$member_id}\]@?{$member_name}\[\/user\]#i", "<a contenteditable=\"false\" rel=\"\" href=\"{$urls[$member_name]}\" data-mentionid=\"{$maps[$member_name]}\" data-ipshover-target=\"{$cardUrls[$member_name]}\" data-ipshover=\"\">@{$member_name}</a>", $post );
$post = preg_replace( "#\[user={$member_id}\]@?{$memberNameEscaped}\[\/user\]#i", "<a contenteditable=\"false\" rel=\"\" href=\"{$urls[ $member_name ]}\" data-mentionid=\"{$maps[ $member_name ]}\" data-ipshover-target=\"{$cardUrls[ $member_name ]}\" data-ipshover=\"\">@{$member_name}</a>", $post );
}
}
}
@@ -530,18 +605,52 @@ class _Xenforo extends \IPS\convert\Software
/* We need to figure out where our file lives - if it's remote, then we need to use file_get_contents() and pass the raw data. */
$filepath = NULL;
$filedata = NULL;
if ( mb_substr( $row['image_url'], 0, 4 ) == 'http' )
$filepathx2 = NULL;
$filedatax2 = NULL;
$filenamex2 = NULL;
/* Sprites */
if( $row['sprite_mode'] )
{
$images = $this->_emoticonsFromSprite( rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . '/' . $row['image_url'], \unserialize( $row['sprite_params'] ) );
$filedata = $images['emoticon'];
if( isset( $images['emoticon_x2'] ) )
{
$filedatax2 = $images['emoticon_x2'];
/* We always save the sprites to PNG */
$filenamex2 = "{$row['title']}.png";
}
/* We always save the sprites to PNG */
$filename = "{$row['title']}.png";
}
elseif ( mb_substr( $row['image_url'], 0, 4 ) == 'http' )
{
$filedata = file_get_contents( $row['image_url'] );
$fileurl = explode( '/', $row['image_url'] );
$filename = array_pop( $fileurl );
if( static::$isLegacy == FALSE AND !empty( $row['image_url_2x'] ) )
{
$filedata = file_get_contents( $row['image_url_2x'] );
$fileurlx2 = explode( '/', $row['image_url_2x'] );
$filenamex2 = array_pop( $fileurlx2 );
}
}
else
{
$fileurl = explode( '/', $row['image_url'] );
$filename = array_pop( $fileurl );
$filepath = rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . '/' . implode( '/', $fileurl );
if( static::$isLegacy == FALSE AND !empty( $row['image_url_2x'] ) )
{
$fileurlx2 = explode( '/', $row['image_url_2x'] );
$filenamex2 = array_pop( $fileurlx2 );
$filepathx2 = rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . '/' . implode( '/', $fileurlx2 );
}
}
/* XenForo allows multiple codes - we don't. */
@@ -552,7 +661,7 @@ class _Xenforo extends \IPS\convert\Software
try
{
$category = $this->db->select( '*', 'xf_smilie_category', array( "smilie_category_id=?", $row['smilie_category_id'] ) )->first();
$title = $this->getPhrase( "smilie_category_{$category['smilie_category_id']}_title" );
$title = $this->getPhrase( "smilie_category_{$category['smilie_category_id']}_title", "smilie_category_title.{$category['smilie_category_id']}" );
if ( is_null( $title ) )
{
@@ -579,10 +688,11 @@ class _Xenforo extends \IPS\convert\Software
'id' => $row['smilie_id'],
'typed' => $code,
'filename' => $filename,
'filenamex2' => $filenamex2,
'emo_position' => $row['display_order'],
);
$libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $filepath, $filedata );
$libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $filepath, $filedata, $filepathx2, $filedatax2 );
$libraryClass->setLastKeyValue( $row['smilie_id'] );
}
@@ -603,8 +713,8 @@ class _Xenforo extends \IPS\convert\Software
$info['pf_id'] = $row['field_id'];
$merge = $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] != 'none' ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] : NULL;
$info['pf_type'] = $this->_fieldMap( $row['field_type'] );
$info['pf_name'] = $this->getPhrase( "user_field_{$row['field_id']}" );
$info['pf_desc'] = $this->getPhrase( "user_field_{$row['field_id']}_desc" );
$info['pf_name'] = $this->getPhrase( "user_field_{$row['field_id']}", "user_field_title.{$row['field_id']}" );
$info['pf_desc'] = $this->getPhrase( "user_field_{$row['field_id']}_desc", "user_field_desc.{$row['field_id']}" );
$info['pf_content'] = ( !in_array( $row['field_type'], array( 'textbox', 'textarea' ) ) ) ? \unserialize( $row['field_choices'] ) : NULL;
$info['pf_not_null'] = $row['required'];
$info['pf_member_hide'] = ( $row['viewable_profile'] > 0 ) ? 0 : 1;
@@ -734,7 +844,7 @@ class _Xenforo extends \IPS\convert\Software
*
* @return void
*/
public function convertWarnReason()
public function convertWarnReasons()
{
$libraryClass = $this->getLibrary();
@@ -742,7 +852,7 @@ class _Xenforo extends \IPS\convert\Software
foreach( $this->fetch( 'xf_warning_definition', 'warning_definition_id' ) AS $row )
{
$title = $this->getPhrase( "warning_definition_{$row['warning_definition_id']}_title" );
$title = $this->getPhrase( "warning_definition_{$row['warning_definition_id']}_title", "warning_title.{$row['warning_definition_id']}" );
$remove = ( $row['expiry_type'] == 'never' ) ? 0 : $row['expiry_default'];
switch( $row['expiry_type'] )
@@ -777,7 +887,7 @@ class _Xenforo extends \IPS\convert\Software
/**
* @brief Member history type map
*/
protected $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'user_group_id' => 'member_group_id', 'secondar_group_ids' => 'mgroup_others' );
protected static $changeLogTypes = array( 'username' => 'display_name', 'email' => 'email_change', 'user_group_id' => 'member_group_id', 'secondary_group_ids' => 'mgroup_others' );
/**
* Convert member history
@@ -790,47 +900,27 @@ class _Xenforo extends \IPS\convert\Software
$libraryClass::setKey( 'log_id' );
$fixGroups = function( $string )
$userField = 'user_id';
$table = 'xf_user_change_log';
if( ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) )
{
$groups = explode( ',', $string );
$newGroups = array();
array_walk( $groups, function( &$item, $key ) use ( &$newGroups )
{
try
{
$item = $this->app->getLink( $item, 'core_groups' );
$newGroups[] = $item;
}
catch( \OutOfRangeException $e ){}
});
return implode( ',', $newGroups );
};
foreach( $this->fetch( 'xf_user_change_log', 'log_id' ) AS $change )
$table = 'xf_change_log';
$userField = 'content_id';
$where = array( 'content_type=? AND ' . $this->db->in( 'field', array_keys( static::$changeLogTypes ) ), 'user' );
}
else
{
if( !in_array( $change['field'], array_keys( $this->changeLogTypes ) ) )
{
$libraryClass->setLastKeyValue( $change['log_id'] );
continue;
}
/* Any processing we need to do */
switch( $this->changeLogTypes[ $change['field'] ] )
{
case 'member_group_id':
case 'mgroup_others':
$change['old_value'] = $fixGroups( $change['old_value'] );
$change['new_value'] = $fixGroups( $change['new_value'] );
break;
}
$where = array( $this->db->in( 'field', array_keys( static::$changeLogTypes ) ) );
}
foreach( $this->fetch( $table, 'log_id', $where ) AS $change )
{
$libraryClass->convertMemberHistory( array(
'log_id' => $change['log_id'],
'log_member' => $change['user_id'],
'log_by' => ( $change['edit_user_id'] !== $change['user_id'] ) ? $change['edit_user_id'] : null,
'log_type' => $this->changeLogTypes[ $change['field'] ],
'log_member' => $change[ $userField ],
'log_by' => ( $change['edit_user_id'] !== $change[ $userField ] ) ? $change['edit_user_id'] : null,
'log_type' => static::$changeLogTypes[ $change['field'] ],
'log_data' => array( 'old' => $change['old_value'], 'new' => $change['new_value'] ),
'log_date' => $change['edit_date']
)
@@ -886,8 +976,8 @@ class _Xenforo extends \IPS\convert\Software
catch( \UnderflowException $e )
{
/* Ut oh... do something random. */
$hash = md5( uniqid() );
$salt = md5( uniqid() );
$hash = md5( mt_rand() );
$salt = md5( mt_rand() );
}
/* IP Address */
@@ -1415,4 +1505,103 @@ class _Xenforo extends \IPS\convert\Software
return NULL;
}
/**
* Process a login
*
* @param \IPS\Member $member The member
* @param string $password Password from form
* @return bool
*/
public function login( $member, $password )
{
$password = html_entity_decode( $password );
// XF 1.2
require_once \IPS\ROOT_PATH . "/applications/convert/sources/Login/PasswordHash.php";
$ph = new \PasswordHash( 10, TRUE );
if ( $ph->CheckPassword( $password, $member->conv_password ) )
{
return TRUE;
}
// XF 1.1
if ( extension_loaded( 'hash' ) )
{
$hashedPassword = hash( 'sha256', hash( 'sha256', $password ) . $member->misc );
}
else
{
$hashedPassword = sha1( sha1( $password ) . $member->misc );
}
if ( \IPS\Login::compareHashes( $member->conv_password, $hashedPassword ) )
{
return TRUE;
}
// If they converted vB > XF > IPB then passwords may be in vB format still - try that.
return \IPS\convert\Software\Core\Vbulletin::login( $member, $password );
}
/**
* @brief Cache sprite image objects
*/
protected $_spriteImages = array();
/**
* Return image from sprite - We must use GD for this, it's generally available on most servers
* And we don't have any relevant methods in either image handler for doing this completly within
* the image class
*
* @param string $sprite Sprite path
* @param array $spriteParams Sprite parameters
* @return array
*/
protected function _emoticonsFromSprite( $sprite, $spriteParams )
{
$key = md5( $sprite );
/* Set up image canvas */
if( !isset( $this->_spriteImages[ $key ] ) )
{
if( !file_exists( $sprite ) )
{
return array( 'emoticon' => NULL );
}
$this->_spriteImages[ $key ] = new \IPS\Image\Gd( file_get_contents( $sprite ) );
}
/* x2 Emoticon? */
$multiplier = 1;
if( $this->_spriteImages[ $key ]->width > $spriteParams['w'] )
{
$multiplier = 2;
}
$emoticon = \IPS\Image\Gd::newImageCanvas( $spriteParams['w'] * $multiplier, $spriteParams['h'] * $multiplier, array( 0, 0, 0 ) );
/* Set the background to transparent */
imagefill( $emoticon->image, 0, 0, imagecolorallocatealpha( $emoticon->image, 0, 0, 0, 127 ) );
/* Extract sprite */
imagecopy( $emoticon->image, $this->_spriteImages[ $key ]->image, 0, 0, abs( $spriteParams['x'] ) * $multiplier, abs( $spriteParams['y'] ) * $multiplier, $spriteParams['w'] * $multiplier, $spriteParams['h'] * $multiplier );
/* x2 Emoticon? */
$return = array();
if( $multiplier == 2 )
{
$return['emoticon_x2'] = (string) $emoticon;
/* Resize */
$emoticon->resize( $spriteParams['w'], $spriteParams['h'] );
}
$return['emoticon'] = (string) $emoticon;
unset( $emoticon );
return $return;
}
}