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 ) ->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' ) ->first(); $infos['register_date'] = $wp->user_registered ? strtotime($wp->user_registered) : null; $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; } }