2026-06-23 19:24:38 +02:00
< ? php
namespace App\Console\Commands ;
use App\Models\MigrationUserPlan ;
use App\Services\XenforoApiService ;
use Illuminate\Console\Attributes\Description ;
use Illuminate\Console\Attributes\Signature ;
use Illuminate\Console\Command ;
use Illuminate\Support\Facades\DB ;
use Illuminate\Support\Facades\Log ;
use Illuminate\Support\Str ;
#[Signature('migrate:users:execute {--limit=} {--wp-uploads-path=} {--xf-data-path=}')]
#[Description("Migrate all users in migration table to XenForo.")]
class MigrateUsersExecute extends Command
{
private function extractWpRole ( ? string $serialized ) : string
{
$caps = $serialized ? @ unserialize ( $serialized ) : null ;
return is_array ( $caps ) ? ( array_key_first ( $caps ) ? : 'contributor' ) : 'contributor' ;
}
private function extractWpAvatarPath ( ? string $serialized , string $wpUploadsPath ) : ? string
{
$data = $serialized ? @ unserialize ( $serialized ) : null ;
$mediaId = $data [ 'media_id' ] ? ? null ;
if ( ! $mediaId )
return null ;
$relative = DB :: connection ( 'old_wp' )
-> table ( 'postmeta' )
-> where ( 'post_id' , $mediaId )
-> where ( 'meta_key' , '_wp_attached_file' )
-> value ( 'meta_value' );
if ( ! $relative )
return null ;
$path = rtrim ( $wpUploadsPath , '/' ) . '/' . $relative ;
return is_file ( $path ) ? $path : null ;
}
private function extractXfAvatarPath ( int $userId , string $xfDataPath , string $size = 'o' ) : ? string
{
$path = sprintf ( '%s/avatars/%s/%d/%d.jpg' ,
rtrim ( $xfDataPath , '/' ), $size , ( int ) floor ( $userId / 1000 ), $userId
);
return is_file ( $path ) ? $path : null ;
}
private function extractXfBannerPath ( int $userId , string $xfDataPath , string $size = 'l' ) : ? string
{
$path = sprintf ( '%s/profile_banners/%s/%d/%d.jpg' ,
rtrim ( $xfDataPath , '/' ), $size , ( int ) floor ( $userId / 1000 ), $userId
);
return is_file ( $path ) ? $path : null ;
}
private function buildUserInfos ( MigrationUserPlan $plan , array $roleMap , array $groupMap , string $wpUploadsPath , string $xfDataPath ) : array
{
$infos = [
'username' => $plan -> xf_username ? : $plan -> wp_username ,
'email' => $plan -> email ,
'user_group_id' => null ,
'password' => Str :: uuid (), // Used for API verifications.
'xf_user_id' => 0 ,
'avatar_path' => null ,
'banner_path' => null ,
'register_date' => null ,
'profile' => [
'about' => null ,
'website' => null ,
],
'source_real_password' => null ,
'wp_password' => null ,
'xf_scheme_class' => null ,
'xf_password_data' => null ,
];
if ( $plan -> wp_user_id )
{
$wp = DB :: connection ( 'old_wp' )
-> table ( 'users' )
-> leftJoin ( 'usermeta as m1' , fn ( $j ) => $j -> on ( 'users.ID' , '=' , 'm1.user_id' ) -> where ( 'm1.meta_key' , '=' , 'description' ) )
-> leftJoin ( 'usermeta as m2' , fn ( $j ) => $j -> on ( 'users.ID' , '=' , 'm2.user_id' ) -> where ( 'm2.meta_key' , '=' , 'wp_capabilities' ) )
-> leftJoin ( 'usermeta as m3' , fn ( $j ) => $j -> on ( 'users.ID' , '=' , 'm3.user_id' ) -> where ( 'm3.meta_key' , '=' , 'simple_local_avatar' ) )
-> where ( 'users.ID' , '=' , $plan -> wp_user_id )
2026-06-25 13:06:14 +02:00
-> select ( 'm1.meta_value as description' , 'm2.meta_value as capabilities' , 'm3.meta_value as avatar_meta' , 'users.user_url as website' , 'users.user_pass as password' , 'users.user_registered' )
2026-06-23 19:24:38 +02:00
-> first ();
2026-06-25 13:06:14 +02:00
$infos [ 'register_date' ] = $wp -> user_registered ? strtotime ( $wp -> user_registered ) : null ;
2026-06-23 19:24:38 +02:00
$infos [ 'profile' ][ 'about' ] = $wp -> description ;
$infos [ 'profile' ][ 'website' ] = $wp -> website ;
$role = $this -> extractWpRole ( $wp -> capabilities );
$infos [ 'user_group_id' ] = $roleMap [ $role ] ? ? $roleMap [ 'contributor' ];
if ( $url = $this -> extractWpAvatarPath ( $wp -> avatar_meta , $wpUploadsPath )){
$infos [ 'avatar_path' ] = $url ;
}
$infos [ 'source_real_password' ] = 'wp' ;
$infos [ 'wp_password' ] = $wp -> password ;
}
if ( $plan -> xf_user_id )
{
$xf = DB :: connection ( 'old_xf' )
-> table ( 'user' )
-> leftJoin ( 'user_profile' , 'user.user_id' , '=' , 'user_profile.user_id' )
-> leftJoin ( 'user_authenticate' , 'user.user_id' , '=' , 'user_authenticate.user_id' )
-> where ( 'user.user_id' , '=' , $plan -> xf_user_id )
-> select ( 'user.avatar_date' , 'user.user_group_id' , 'user.register_date' , 'user_profile.about' , 'user_profile.website' , 'user_profile.banner_date' , 'user_authenticate.scheme_class' , 'user_authenticate.data' )
-> first ();
if ( ! $infos [ 'register_date' ] && $xf )
$infos [ 'register_date' ] = $xf -> register_date ? : null ;
if ( ! $infos [ 'profile' ][ 'about' ] && $xf )
$infos [ 'profile' ][ 'about' ] = $xf -> about ? : null ;
if ( ! $infos [ 'profile' ][ 'website' ] && $xf )
$infos [ 'profile' ][ 'website' ] = $xf -> website ? : null ;
if ( ! $plan -> wp_user_id ){
$infos [ 'user_group_id' ] = $groupMap [ $xf -> user_group_id ] ? ? reset ( $groupMap );
}
$infos [ 'xf_user_id' ] = $plan -> xf_user_id ;
if ( $infos [ 'avatar_path' ] === null && ( int ) $xf -> avatar_date > 0 ){
if ( $path = $this -> extractXfAvatarPath ( $plan -> xf_user_id , $xfDataPath )){
$infos [ 'avatar_path' ] = $path ;
}
}
if ( $infos [ 'banner_path' ] === null && ( int ) $xf -> banner_date > 0 ){
if ( $path = $this -> extractXfBannerPath ( $plan -> xf_user_id , $xfDataPath )){
$infos [ 'banner_path' ] = $path ;
}
}
if ( $infos [ 'source_real_password' ] === null && $xf -> scheme_class && $xf -> data ){
$infos [ 'source_real_password' ] = 'xf' ;
$infos [ 'xf_scheme_class' ] = $xf -> scheme_class ;
$infos [ 'xf_password_data' ] = $xf -> data ;
}
}
return $infos ;
}
private function logMap ( string $sourceSystem , string $sourceTable , int $sourceId , int $targetId )
{
DB :: table ( 'migrations_logs' ) -> insert ([
'source_system' => $sourceSystem ,
'source_table' => $sourceTable ,
'source_id' => $sourceId ,
'target_table' => 'xf_user' ,
'target_id' => $targetId ,
'status' => 'done' ,
'migrated_at' => now (),
'created_at' => now (),
'updated_at' => now ()
]);
}
public function handle ()
{
$wpUploadsPath = $this -> option ( 'wp-uploads-path' );
$xfDataPath = $this -> option ( 'xf-data-path' );
if ( ! $wpUploadsPath || ! is_dir ( $wpUploadsPath ) ){
$this -> error ( 'Missing WP Uploads Path' );
return self :: FAILURE ;
}
if ( ! $xfDataPath || ! is_dir ( $xfDataPath ) ){
$this -> error ( 'Missing XF Data Path' );
return self :: FAILURE ;
}
$roleMap = json_decode ( DB :: table ( 'migration_settings' ) -> where ( 'key' , 'wp_role_to_xf_group' ) -> value ( 'value' ), true );
$groupMap = json_decode ( DB :: table ( 'migration_settings' ) -> where ( 'key' , 'old_xf_group_to_xf_group' ) -> value ( 'value' ), true );
if ( ! $roleMap || ! $groupMap ) {
$this -> error ( 'Role map and group map are required.' );
return self :: FAILURE ;
}
$query = MigrationUserPlan :: where ( 'status' , 'approved' ) -> whereNull ( 'user_id' );
if ( $limit = $this -> option ( 'limit' ) ) {
$query -> limit (( int ) $limit );
}
$rows = $query -> get ();
$this -> info ( " { $rows -> count () } accounts will be created on XenForo database !!!. " );
$ok = $this -> ask ( " Write 'ok' if you want to start the migration. Everything else to quit the migration. " );
if ( $ok !== 'ok' ) {
return self :: SUCCESS ;
}
$service = app ( XenforoApiService :: class );
$this -> withProgressBar ( $rows , function ( $plan ) use ( $roleMap , $groupMap , $wpUploadsPath , $xfDataPath , $service ) {
try {
$infos = $this -> buildUserInfos ( $plan , $roleMap , $groupMap , $wpUploadsPath , $xfDataPath );
[ $userId , $passwordSet ] = $service -> _migrateUser ( $infos );
if ( ! $userId ){
throw new \RuntimeException ( " Error when user creation. " );
}
MigrationUserPlan :: where ( 'id' , $plan -> id ) -> update ([ 'user_id' => $userId ]);
$this -> logMap ( $plan -> wp_user_id ? 'wp' : 'xf' , $plan -> wp_user_id ? 'wp_users' : 'xf_user' , $plan -> wp_user_id ? ? $plan -> xf_user_id , $userId );
if ( $plan -> wp_user_id && $plan -> xf_user_id ){
$this -> logMap ( 'xf' , 'xf_user' , $plan -> xf_user_id , $userId );
}
} catch ( \Throwable $e ) {
Log :: error ( " Unable to create Plan# { $plan -> id } user : { $e -> getMessage () } " );
}
});
$this -> newLine ( 2 );
$this -> info ( " Process finished. " );
return self :: SUCCESS ;
}
}