diff --git a/assets/Module-guest-view.png b/assets/Module-guest-view.png
new file mode 100644
index 0000000..5d8cf94
Binary files /dev/null and b/assets/Module-guest-view.png differ
diff --git a/assets/Module-logged-view.png b/assets/Module-logged-view.png
new file mode 100644
index 0000000..57469f1
Binary files /dev/null and b/assets/Module-logged-view.png differ
diff --git a/assets/Module_configuration.png b/assets/Module_configuration.png
new file mode 100644
index 0000000..9b75afc
Binary files /dev/null and b/assets/Module_configuration.png differ
diff --git a/assets/Module_installation.png b/assets/Module_installation.png
new file mode 100644
index 0000000..f887d81
Binary files /dev/null and b/assets/Module_installation.png differ
diff --git a/assets/Module_management.png b/assets/Module_management.png
new file mode 100644
index 0000000..7f5478d
Binary files /dev/null and b/assets/Module_management.png differ
diff --git a/composer.json b/composer.json
index 219eb42..ec921bb 100644
--- a/composer.json
+++ b/composer.json
@@ -17,17 +17,18 @@
},
"autoload": {
"psr-4": {
- "Binary\\Component\\CmsMigrator\\": "src/component/admin/src/"
+ "Joomla\\Component\\CmsMigrator\\Administrator\\": "src/component/admin/src/"
}
},
"require-dev": {
"phpunit/phpunit": "^10",
"joomla/test": "^2",
- "mockery/mockery": "^1.6"
+ "mockery/mockery": "^1.6",
+ "squizlabs/php_codesniffer": "*"
},
"autoload-dev": {
"psr-4": {
- "Binary\\Component\\CmsMigrator\\Tests\\": "tests/"
+ "Joomla\\Component\\CmsMigrator\\Tests\\": "tests/unit/"
}
},
"scripts": {
diff --git a/docs/Migration_Notice_Guide.md b/docs/Migration_Notice_Guide.md
new file mode 100644
index 0000000..6cad4f2
--- /dev/null
+++ b/docs/Migration_Notice_Guide.md
@@ -0,0 +1,74 @@
+# Migration Notice Module - User Manual
+
+The Migration Notice Module helps to explain password reset requirements to end users after migrating from Source into Joomla. It displays clear instructions and reduces login issues.
+
+---
+
+## π Installation
+
+### Step 1: Install the Module
+
+#### The Module is by default installed with the com_cmsmigrator component. If you need to install it separately, follow these steps:
+
+1. **Login** to Joomla Administrator
+2. **Go to** Extensions β Manage β Install
+3. **Upload** the ZIP file (`mod_migrationnotice_v1.0.0.zip`)
+4. **Click** Install
+
+
+
+β
**Success**: Language overrides are automatically created for login error messages.
+
+---
+
+## βοΈ Configuration
+
+### Step 2: Set Up the Module
+1. **Go to** Extensions β Modules
+2. **Click** "New" β Select "Migration Notice Module"
+3. **Configure** these key settings:
+
+
+
+| Setting | Recommended Value |
+|---------|------------------|
+| **Show Migration Notice** | Yes |
+| **Notice Type** | Both |
+| **Alert Style** | Warning |
+| **Show Password Reset Link** | Yes |
+| **Menu Assignment** | All Pages |
+| **Position** | Top |
+
+4. **Save & Close**
+
+---
+
+## π What Users See
+
+### Guest Users (Not Logged In)
+
+
+Shows:
+- Migration explanation
+- Password reset requirement
+- Login button
+
+### Logged-In Users
+
+
+Shows:
+- Password reset notice
+- Direct "Reset Password" button
+
+## π§ Management
+
+### Editing the Module
+**Extensions** β **Modules** β **Find your module** β **Click title to edit**
+
+
+
+### Common Tasks
+- **Hide temporarily**: Set "Show Migration Notice" to No
+- **Change message**: Use "Custom Message" field
+- **Different pages**: Adjust "Menu Assignment"
+- **New position**: Change "Position" setting
diff --git a/src/cms-export-plugins/wp-all-export/wp-all-export.php b/src/cms-export-plugins/wp-all-export/wp-all-export.php
new file mode 100644
index 0000000..5600370
--- /dev/null
+++ b/src/cms-export-plugins/wp-all-export/wp-all-export.php
@@ -0,0 +1,938 @@
+
+
+
+
Select the data you want to export and click the "Export" button.
+
+
+
+ $batch_size,
+ 'offset' => $offset,
+ 'fields' => 'all'
+ ) );
+
+ if ( empty( $users ) ) {
+ return array();
+ }
+
+ // Preload user meta cache
+ $user_ids = wp_list_pluck( $users, 'ID' );
+ update_meta_cache( 'user', $user_ids );
+
+ $rows = array();
+ foreach ( $users as $user ) {
+ $rows[] = array(
+ $user->ID,
+ $user->user_login,
+ $user->user_email,
+ $user->display_name,
+ $user->user_pass,
+ json_encode( get_user_meta( $user->ID ) )
+ );
+ }
+
+ return $rows;
+}
+
+function aex_export_posts() {
+ $filename = 'posts-export-' . date('Y-m-d') . '.csv';
+ $headers = array('ID', 'Title', 'Content', 'Excerpt', 'Date', 'Status', 'Permalink', 'Featured Image URL', 'Metadata');
+
+ aex_generic_csv_export( $filename, $headers, 'aex_get_posts_batch' );
+}
+
+// Batch callback for posts export
+function aex_get_posts_batch( $offset, $batch_size ) {
+ $page = floor( $offset / $batch_size ) + 1;
+
+ $query = new WP_Query( array(
+ 'post_type' => 'post',
+ 'post_status' => 'any',
+ 'posts_per_page' => $batch_size,
+ 'paged' => $page,
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false, // We'll do this manually for better control
+ 'update_post_term_cache' => false
+ ) );
+
+ if ( !$query->have_posts() ) {
+ return array();
+ }
+
+ $posts = $query->posts;
+
+ // Preload post meta cache
+ $post_ids = wp_list_pluck( $posts, 'ID' );
+ update_meta_cache( 'post', $post_ids );
+
+ $rows = array();
+ foreach ( $posts as $post ) {
+ $rows[] = array(
+ $post->ID,
+ $post->post_title,
+ $post->post_content,
+ $post->post_excerpt,
+ $post->post_date,
+ $post->post_status,
+ get_permalink( $post->ID ),
+ get_the_post_thumbnail_url( $post->ID, 'full' ),
+ json_encode( get_post_meta( $post->ID ) )
+ );
+ }
+
+ wp_reset_postdata();
+ return $rows;
+}
+
+function aex_export_pages() {
+ $filename = 'pages-export-' . date('Y-m-d') . '.csv';
+ $headers = array('ID', 'Title', 'Content', 'Excerpt', 'Date', 'Status', 'Permalink', 'Featured Image URL', 'Metadata');
+
+ aex_generic_csv_export( $filename, $headers, 'aex_get_pages_batch' );
+}
+
+// Batch callback for pages export
+function aex_get_pages_batch( $offset, $batch_size ) {
+ $page = floor( $offset / $batch_size ) + 1;
+
+ $query = new WP_Query( array(
+ 'post_type' => 'page',
+ 'post_status' => 'any',
+ 'posts_per_page' => $batch_size,
+ 'paged' => $page,
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false
+ ) );
+
+ if ( !$query->have_posts() ) {
+ return array();
+ }
+
+ $pages = $query->posts;
+
+ // Preload post meta cache
+ $post_ids = wp_list_pluck( $pages, 'ID' );
+ update_meta_cache( 'post', $post_ids );
+
+ $rows = array();
+ foreach ( $pages as $page ) {
+ $rows[] = array(
+ $page->ID,
+ $page->post_title,
+ $page->post_content,
+ $page->post_excerpt,
+ $page->post_date,
+ $page->post_status,
+ get_permalink( $page->ID ),
+ get_the_post_thumbnail_url( $page->ID, 'full' ),
+ json_encode( get_post_meta( $page->ID ) )
+ );
+ }
+
+ wp_reset_postdata();
+ return $rows;
+}
+
+function aex_export_categories() {
+ $filename = 'categories-export-' . date('Y-m-d') . '.csv';
+ $headers = array('ID', 'Name', 'Slug', 'Description', 'Count', 'Metadata');
+
+ aex_generic_csv_export( $filename, $headers, 'aex_get_categories_batch' );
+}
+
+// Batch callback for categories export
+function aex_get_categories_batch( $offset, $batch_size ) {
+ $categories = get_terms( array(
+ 'taxonomy' => 'category',
+ 'hide_empty' => false,
+ 'number' => $batch_size,
+ 'offset' => $offset
+ ) );
+
+ if ( empty( $categories ) || is_wp_error( $categories ) ) {
+ return array();
+ }
+
+ // Preload term meta cache
+ $term_ids = wp_list_pluck( $categories, 'term_id' );
+ update_meta_cache( 'term', $term_ids );
+
+ $rows = array();
+ foreach ( $categories as $category ) {
+ $rows[] = array(
+ $category->term_id,
+ $category->name,
+ $category->slug,
+ $category->description,
+ $category->count,
+ json_encode( get_term_meta( $category->term_id ) )
+ );
+ }
+
+ return $rows;
+}
+
+function aex_export_taxonomies() {
+ $filename = 'taxonomies-export-' . date('Y-m-d') . '.csv';
+ $headers = array('Taxonomy', 'Term ID', 'Name', 'Slug', 'Description', 'Count', 'Metadata');
+
+ aex_generic_csv_export( $filename, $headers, 'aex_get_taxonomies_batch' );
+}
+
+// Global variable to track taxonomy export state
+$aex_taxonomy_export_state = array(
+ 'taxonomies' => array(),
+ 'current_taxonomy_index' => 0,
+ 'current_taxonomy_offset' => 0
+);
+
+// Batch callback for taxonomies export
+function aex_get_taxonomies_batch( $offset, $batch_size ) {
+ global $aex_taxonomy_export_state;
+
+ // Initialize taxonomies on first call
+ if ( empty( $aex_taxonomy_export_state['taxonomies'] ) ) {
+ $aex_taxonomy_export_state['taxonomies'] = get_taxonomies( array( 'public' => true ), 'objects' );
+ $aex_taxonomy_export_state['current_taxonomy_index'] = 0;
+ $aex_taxonomy_export_state['current_taxonomy_offset'] = 0;
+ }
+
+ $taxonomies = $aex_taxonomy_export_state['taxonomies'];
+ $current_index = $aex_taxonomy_export_state['current_taxonomy_index'];
+ $current_offset = $aex_taxonomy_export_state['current_taxonomy_offset'];
+
+ if ( $current_index >= count( $taxonomies ) ) {
+ return array(); // No more taxonomies to process
+ }
+
+ $rows = array();
+ $rows_collected = 0;
+
+ while ( $rows_collected < $batch_size && $current_index < count( $taxonomies ) ) {
+ $taxonomy_names = array_keys( $taxonomies );
+ $taxonomy_name = $taxonomy_names[$current_index];
+ $taxonomy = $taxonomies[$taxonomy_name];
+
+ $terms = get_terms( array(
+ 'taxonomy' => $taxonomy->name,
+ 'hide_empty' => false,
+ 'number' => $batch_size - $rows_collected,
+ 'offset' => $current_offset
+ ) );
+
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ // Move to next taxonomy
+ $current_index++;
+ $current_offset = 0;
+ continue;
+ }
+
+ // Preload term meta cache
+ $term_ids = wp_list_pluck( $terms, 'term_id' );
+ update_meta_cache( 'term', $term_ids );
+
+ foreach ( $terms as $term ) {
+ $rows[] = array(
+ $taxonomy->name,
+ $term->term_id,
+ $term->name,
+ $term->slug,
+ $term->description,
+ $term->count,
+ json_encode( get_term_meta( $term->term_id ) )
+ );
+ $rows_collected++;
+
+ if ( $rows_collected >= $batch_size ) {
+ break;
+ }
+ }
+
+ if ( count( $terms ) < ( $batch_size - ( $rows_collected - count( $terms ) ) ) ) {
+ // This taxonomy is exhausted, move to next
+ $current_index++;
+ $current_offset = 0;
+ } else {
+ // More terms in this taxonomy
+ $current_offset += count( $terms );
+ }
+ }
+
+ // Update global state
+ $aex_taxonomy_export_state['current_taxonomy_index'] = $current_index;
+ $aex_taxonomy_export_state['current_taxonomy_offset'] = $current_offset;
+
+ return $rows;
+}
+
+function aex_export_media() {
+ // Check if ZipArchive class is available
+ if ( ! class_exists( 'ZipArchive' ) ) {
+ wp_die( 'ZipArchive class is not available. Please enable the ZIP extension in your PHP installation.' );
+ }
+
+ // Initialize WordPress Filesystem
+ if ( ! function_exists( 'WP_Filesystem' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ }
+
+ WP_Filesystem();
+ global $wp_filesystem;
+
+ if ( ! $wp_filesystem ) {
+ wp_die( 'Cannot initialize WordPress Filesystem.' );
+ }
+
+ $upload_dir = wp_upload_dir();
+ $uploads_path = $upload_dir['basedir'];
+
+ // Check if uploads directory exists
+ if ( ! $wp_filesystem->is_dir( $uploads_path ) ) {
+ wp_die( 'Uploads directory not found.' );
+ }
+
+ $filename = 'all-media-' . date( 'Y-m-d' ) . '.zip';
+
+ // Create cache directory if it doesn't exist
+ $cache_dir = WP_CONTENT_DIR . '/cache';
+ if ( ! $wp_filesystem->is_dir( $cache_dir ) ) {
+ $wp_filesystem->mkdir( $cache_dir, 0755 );
+ }
+
+ $temp_file = $cache_dir . '/' . $filename;
+
+ $zip = new ZipArchive();
+ if ( $zip->open( $temp_file, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== TRUE ) {
+ wp_die( 'Cannot create zip file.' );
+ }
+
+ // Use WordPress Filesystem to recursively add files to zip
+ aex_add_files_to_zip_optimized( $zip, $uploads_path, $uploads_path, $wp_filesystem );
+
+ $zip->close();
+
+ // Check if zip file was created successfully
+ if ( ! $wp_filesystem->exists( $temp_file ) ) {
+ wp_die( 'Failed to create zip file.' );
+ }
+
+ // Set headers for download
+ header( 'Content-Type: application/zip' );
+ header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
+ header( 'Content-Length: ' . $wp_filesystem->size( $temp_file ) );
+ header( 'Cache-Control: no-cache, must-revalidate' );
+ header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
+
+ // Output the file in chunks to handle large files
+ $handle = $wp_filesystem->get_contents( $temp_file );
+ echo $handle;
+
+ // Clean up temporary file
+ $wp_filesystem->delete( $temp_file );
+ exit;
+}
+
+// Optimized helper function to recursively add files to zip using WordPress Filesystem
+function aex_add_files_to_zip_optimized( $zip, $source_path, $base_path, $wp_filesystem ) {
+ $files = aex_get_media_files_optimized( $source_path, $wp_filesystem );
+
+ foreach ( $files as $file ) {
+ $relative_path = substr( $file, strlen( $base_path ) + 1 );
+
+ // Skip hidden files and system files
+ if ( strpos( $relative_path, '.' ) === 0 || strpos( $relative_path, '__MACOSX' ) !== false ) {
+ continue;
+ }
+
+ // Check if we should include this file (optimized image selection)
+ if ( aex_should_include_file( $relative_path ) ) {
+ $zip->addFile( $file, $relative_path );
+ }
+ }
+}
+
+// Helper function to get media files in batches to avoid memory issues
+function aex_get_media_files_optimized( $source_path, $wp_filesystem ) {
+ $all_files = array();
+ $image_groups = array();
+
+ $files = $wp_filesystem->dirlist( $source_path, true, true );
+
+ if ( ! $files ) {
+ return array();
+ }
+
+ aex_process_directory_recursive( $source_path, $files, $all_files, $image_groups );
+
+ // Select preferred images
+ foreach ( $image_groups as $group ) {
+ $preferred_file = aex_select_preferred_image( $group );
+ if ( $preferred_file ) {
+ $all_files[] = $preferred_file['path'];
+ }
+ }
+
+ return array_unique( $all_files );
+}
+
+// Recursive helper to process directory structure
+function aex_process_directory_recursive( $current_path, $files, &$all_files, &$image_groups ) {
+ foreach ( $files as $name => $file_info ) {
+ $full_path = $current_path . '/' . $name;
+
+ if ( $file_info['type'] === 'd' && isset( $file_info['files'] ) ) {
+ // Directory - recurse
+ aex_process_directory_recursive( $full_path, $file_info['files'], $all_files, $image_groups );
+ } else {
+ // File - process
+ $file_info_parsed = pathinfo( $full_path );
+ $extension = strtolower( $file_info_parsed['extension'] ?? '' );
+ $filename = $file_info_parsed['filename'] ?? '';
+
+ $image_extensions = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg' );
+
+ if ( in_array( $extension, $image_extensions ) ) {
+ // Group images
+ $base_name = aex_get_image_base_name( $filename );
+ $group_key = $file_info_parsed['dirname'] . '/' . $base_name . '.' . $extension;
+
+ if ( ! isset( $image_groups[$group_key] ) ) {
+ $image_groups[$group_key] = array();
+ }
+
+ $image_groups[$group_key][] = array(
+ 'path' => $full_path,
+ 'relative_path' => substr( $full_path, strlen( $current_path ) + 1 ),
+ 'filename' => $filename,
+ 'is_original' => ! preg_match( '/-(\d+)x(\d+)$/', $filename ),
+ 'is_768x512' => preg_match( '/-768x512$/', $filename )
+ );
+ } else {
+ // Non-image file
+ $all_files[] = $full_path;
+ }
+ }
+ }
+}
+
+// Helper function to recursively add files to zip
+function aex_add_files_to_zip( $zip, $source_path, $base_path ) {
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($source_path),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ $all_files = array();
+ $image_groups = array();
+
+ // First pass: collect all files and group images
+ foreach ($iterator as $file) {
+ if (!$file->isDir()) {
+ $file_path = $file->getRealPath();
+ $relative_path = substr($file_path, strlen($base_path) + 1);
+
+ // Skip hidden files and system files
+ if (strpos($relative_path, '.') === 0 || strpos($relative_path, '__MACOSX') !== false) {
+ continue;
+ }
+
+ $file_info = pathinfo($relative_path);
+ $extension = strtolower($file_info['extension']);
+ $filename = $file_info['filename'];
+
+ // Common image extensions
+ $image_extensions = array('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg');
+
+ if (in_array($extension, $image_extensions)) {
+ // This is an image file, group it
+ $base_name = aex_get_image_base_name($filename);
+ $group_key = $file_info['dirname'] . '/' . $base_name . '.' . $extension;
+
+ if (!isset($image_groups[$group_key])) {
+ $image_groups[$group_key] = array();
+ }
+
+ $image_groups[$group_key][] = array(
+ 'path' => $file_path,
+ 'relative_path' => $relative_path,
+ 'filename' => $filename,
+ 'is_original' => !preg_match('/-(\d+)x(\d+)$/', $filename),
+ 'is_768x512' => preg_match('/-768x512$/', $filename)
+ );
+ } else {
+ // Not an image, add directly
+ $all_files[] = array(
+ 'path' => $file_path,
+ 'relative_path' => $relative_path
+ );
+ }
+ }
+ }
+
+ // Second pass: select preferred version for each image group
+ foreach ($image_groups as $group) {
+ $preferred_file = aex_select_preferred_image($group);
+ if ($preferred_file) {
+ $all_files[] = $preferred_file;
+ }
+ }
+
+ // Add all selected files to zip
+ foreach ($all_files as $file) {
+ $zip->addFile($file['path'], $file['relative_path']);
+ }
+}
+
+// Helper function to get the base name of an image (without size suffix)
+function aex_get_image_base_name($filename) {
+ // Remove size suffix like -768x512, -300x200, etc.
+ return preg_replace('/-\d+x\d+$/', '', $filename);
+}
+
+// Helper function to select the preferred image from a group
+function aex_select_preferred_image($image_group) {
+ $preferred = null;
+
+ foreach ($image_group as $image) {
+ if ($image['is_original']) {
+ // Found original version, this is our top preference
+ return $image;
+ } elseif ($image['is_768x512'] && $preferred === null) {
+ // 768x512 version, keep as fallback
+ $preferred = $image;
+ }
+ }
+
+ // Return the 768x512 if no original was found, or the first file if neither
+ return $preferred ?: $image_group[0];
+}
+
+// Helper function to determine if a file should be included in the export
+function aex_should_include_file($file_path) {
+ $file_info = pathinfo($file_path);
+ $extension = strtolower($file_info['extension']);
+ $filename = $file_info['filename'];
+
+ // Common image extensions
+ $image_extensions = array('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg');
+
+ // If it's not an image file, include it (PDFs, documents, etc.)
+ if (!in_array($extension, $image_extensions)) {
+ return true;
+ }
+
+ // For image files, prefer original images without size suffix
+ // WordPress image pattern: filename-{width}x{height}.extension
+ if (preg_match('/-(\d+)x(\d+)$/', $filename, $matches)) {
+ // This is a resized image, only include if no original exists
+ // This function is used in the older method, prefer original images
+ return false;
+ } else {
+ // No size suffix found, this is the original image
+ return true;
+ }
+}
+
+function aex_export_all_json() {
+ $filename = 'all-export-' . date('Y-m-d') . '.json';
+ header('Content-Type: application/json');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Cache-Control: no-cache, must-revalidate');
+ header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+
+ // Start JSON output
+ echo "{\n";
+
+ $sections_output = 0;
+
+ // Export Users
+ echo '"users": [';
+ aex_stream_users_json();
+ echo ']';
+ $sections_output++;
+
+ // Export Post Types
+ if ( $sections_output > 0 ) echo ",\n";
+ echo '"post_types": {';
+ aex_stream_post_types_json();
+ echo '}';
+ $sections_output++;
+
+ // Export Taxonomies
+ if ( $sections_output > 0 ) echo ",\n";
+ echo '"taxonomies": {';
+ aex_stream_taxonomies_json();
+ echo '}';
+ $sections_output++;
+
+ // Export Navigation Menus
+ if ( $sections_output > 0 ) echo ",\n";
+ echo '"navigation_menus": {';
+ aex_stream_navigation_menus_json();
+ echo '}';
+
+ // End JSON output
+ echo "\n}";
+ exit;
+}
+
+// Stream users in batches
+function aex_stream_users_json() {
+ $batch_size = 50;
+ $offset = 0;
+ $first_user = true;
+
+ do {
+ $users = get_users( array(
+ 'number' => $batch_size,
+ 'offset' => $offset,
+ 'fields' => 'all'
+ ) );
+
+ if ( empty( $users ) ) {
+ break;
+ }
+
+ // Preload user meta cache
+ $user_ids = wp_list_pluck( $users, 'ID' );
+ update_meta_cache( 'user', $user_ids );
+
+ foreach ( $users as $user ) {
+ if ( ! $first_user ) {
+ echo ',';
+ }
+ $first_user = false;
+
+ $userdata = $user->to_array();
+ $userdata['user_pass'] = $user->user_pass;
+ $userdata['metadata'] = get_user_meta( $user->ID );
+
+ echo json_encode( $userdata );
+
+ // Flush output to avoid memory buildup
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+
+ $offset += $batch_size;
+
+ } while ( count( $users ) === $batch_size );
+}
+
+// Stream post types in batches
+function aex_stream_post_types_json() {
+ $post_types = get_post_types( array( 'public' => true ), 'names' );
+ $first_post_type = true;
+
+ foreach ( $post_types as $post_type ) {
+ if ( ! $first_post_type ) {
+ echo ',';
+ }
+ $first_post_type = false;
+
+ echo '"' . esc_js( $post_type ) . '": [';
+ aex_stream_posts_for_type_json( $post_type );
+ echo ']';
+
+ // Flush output
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+}
+
+// Stream posts for a specific post type
+function aex_stream_posts_for_type_json( $post_type ) {
+ $batch_size = 50;
+ $page = 1;
+ $first_post = true;
+
+ do {
+ $query = new WP_Query( array(
+ 'post_type' => $post_type,
+ 'post_status' => 'any',
+ 'posts_per_page' => $batch_size,
+ 'paged' => $page,
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false
+ ) );
+
+ if ( ! $query->have_posts() ) {
+ break;
+ }
+
+ $posts = $query->posts;
+
+ // Preload post meta cache
+ $post_ids = wp_list_pluck( $posts, 'ID' );
+ update_meta_cache( 'post', $post_ids );
+
+ foreach ( $posts as $post ) {
+ if ( ! $first_post ) {
+ echo ',';
+ }
+ $first_post = false;
+
+ $postdata = $post->to_array();
+ $postdata['permalink'] = get_permalink( $post->ID );
+ $postdata['metadata'] = get_post_meta( $post->ID );
+ $postdata['featured_image_url'] = get_the_post_thumbnail_url( $post->ID, 'full' );
+
+ $post_taxonomies = get_object_taxonomies( $post, 'objects' );
+ $postdata['terms'] = array();
+ foreach ( $post_taxonomies as $tax_slug => $taxonomy ) {
+ $postdata['terms'][$tax_slug] = wp_get_post_terms( $post->ID, $tax_slug, array( 'fields' => 'all' ) );
+ }
+
+ echo json_encode( $postdata );
+
+ // Flush output
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+
+ wp_reset_postdata();
+ $page++;
+
+ } while ( count( $posts ) === $batch_size );
+}
+
+// Stream taxonomies in batches
+function aex_stream_taxonomies_json() {
+ $taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );
+ $first_taxonomy = true;
+
+ foreach ( $taxonomies as $taxonomy ) {
+ if ( ! $first_taxonomy ) {
+ echo ',';
+ }
+ $first_taxonomy = false;
+
+ echo '"' . esc_js( $taxonomy->name ) . '": [';
+ aex_stream_terms_for_taxonomy_json( $taxonomy->name );
+ echo ']';
+
+ // Flush output
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+}
+
+// Stream terms for a specific taxonomy
+function aex_stream_terms_for_taxonomy_json( $taxonomy_name ) {
+ $batch_size = 100;
+ $offset = 0;
+ $first_term = true;
+
+ do {
+ $terms = get_terms( array(
+ 'taxonomy' => $taxonomy_name,
+ 'hide_empty' => false,
+ 'number' => $batch_size,
+ 'offset' => $offset
+ ) );
+
+ if ( empty( $terms ) || is_wp_error( $terms ) ) {
+ break;
+ }
+
+ // Preload term meta cache
+ $term_ids = wp_list_pluck( $terms, 'term_id' );
+ update_meta_cache( 'term', $term_ids );
+
+ foreach ( $terms as $term ) {
+ if ( ! $first_term ) {
+ echo ',';
+ }
+ $first_term = false;
+
+ $termdata = $term->to_array();
+ $termdata['metadata'] = get_term_meta( $term->term_id );
+
+ echo json_encode( $termdata );
+
+ // Flush output
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+
+ $offset += $batch_size;
+
+ } while ( count( $terms ) === $batch_size );
+}
+
+// Stream navigation menus
+function aex_stream_navigation_menus_json() {
+ $menus = get_terms( array( 'taxonomy' => 'nav_menu', 'hide_empty' => false ) );
+ $first_menu = true;
+
+ foreach ( $menus as $menu ) {
+ if ( ! $first_menu ) {
+ echo ',';
+ }
+ $first_menu = false;
+
+ $menu_items = wp_get_nav_menu_items( $menu->term_id );
+ echo '"' . esc_js( $menu->name ) . '": ' . json_encode( $menu_items );
+
+ // Flush output
+ if ( ob_get_level() ) {
+ ob_flush();
+ }
+ flush();
+ }
+}
\ No newline at end of file
diff --git a/src/component/admin/forms/import.xml b/src/component/admin/forms/import.xml
index 4d84565..5971fbf 100644
--- a/src/component/admin/forms/import.xml
+++ b/src/component/admin/forms/import.xml
@@ -4,10 +4,10 @@
-
+
@@ -15,7 +15,7 @@
@@ -23,8 +23,8 @@
@@ -32,8 +32,8 @@
@@ -43,22 +43,22 @@
-
-
-
-
+
+
+
+
@@ -66,8 +66,8 @@
@@ -85,8 +85,8 @@
@@ -94,8 +94,8 @@
@@ -106,8 +106,8 @@
-
-
+
+
@@ -137,8 +137,8 @@
diff --git a/src/component/admin/language/en-GB/com_cmsmigrator.ini b/src/component/admin/language/en-GB/com_cmsmigrator.ini
index 2dc2dce..132df25 100644
--- a/src/component/admin/language/en-GB/com_cmsmigrator.ini
+++ b/src/component/admin/language/en-GB/com_cmsmigrator.ini
@@ -1,74 +1,106 @@
-COM_CMSMIGRATOR = "Migrate To Joomla"
-COM_CMSMIGRATOR_DESCRIPTION = "This component migrates websites from other platforms to Joomla."
-COM_CMSMIGRATOR_MENU_BACKEND = "Migrate to Joomla"
-COM_CMSMIGRATOR_IMPORT_FILE_ERROR="Error uploading import file."
-COM_CMSMIGRATOR_IMPORT_SUCCESS="Import completed successfully!"
-COM_CMSMIGRATOR_IMPORT_USERS_COUNT="Users imported: %d"
+;
+; @package Joomla.Administrator
+; @subpackage com_cmsmigrator
+; @copyright Copyright (C) 2025 Open Source Matters, Inc.
+; @license GNU General Public License version 2 or later; see LICENSE.txt
+;
+
+COM_CMSMIGRATOR="Migrate To Joomla"
+COM_CMSMIGRATOR_ARTICLE_IMPORTED_SUCCESS="Article '%s' imported successfully."
+COM_CMSMIGRATOR_DESCRIPTION="This component migrates websites from other platforms to Joomla."
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_DESC="Choose between FTP, FTPS, SFTP or ZIP upload for media migration"
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_FTP="FTP (File Transfer Protocol)"
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_FTPS="FTPS (FTP over SSL/TLS)"
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_LABEL="Connection Type"
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_SFTP="SFTP (Secure File Transfer Protocol)"
+COM_CMSMIGRATOR_FIELD_CONNECTION_TYPE_ZIP="ZIP Upload (Upload wp-content/uploads folder as ZIP)"
+COM_CMSMIGRATOR_FIELD_ENABLE_MEDIA_MIGRATION_DESC="Enable media migration from WordPress. Choose FTP/SFTP to download files directly from your server, or ZIP upload to extract a ZIP file containing your wp-content/uploads folder."
+COM_CMSMIGRATOR_FIELD_ENABLE_MEDIA_MIGRATION_LABEL="Enable Media Migration"
+COM_CMSMIGRATOR_FIELD_FTP_HOST_DESC="Server hostname or IP address (e.g., ftp.yourwordpresssite.com)"
+COM_CMSMIGRATOR_FIELD_FTP_HOST_LABEL="Server Host"
+COM_CMSMIGRATOR_FIELD_FTP_PASSWORD_DESC="Login password"
+COM_CMSMIGRATOR_FIELD_FTP_PASSWORD_LABEL="Password"
+COM_CMSMIGRATOR_FIELD_FTP_PASSIVE_DESC="Enable passive mode (FTP/FTPS only - recommended for most FTP servers)"
+COM_CMSMIGRATOR_FIELD_FTP_PASSIVE_LABEL="Use Passive Mode"
+COM_CMSMIGRATOR_FIELD_FTP_PORT_DESC="Server port (FTP default: 21, FTPS default: 21, SFTP default: 22)"
+COM_CMSMIGRATOR_FIELD_FTP_PORT_LABEL="Server Port"
+COM_CMSMIGRATOR_FIELD_FTP_USERNAME_DESC="Login username"
+COM_CMSMIGRATOR_FIELD_FTP_USERNAME_LABEL="Username"
+COM_CMSMIGRATOR_FIELD_IMPORT_AS_SUPER_USER_DESC="If enabled, all imported articles will be assigned to you (the current super user). Otherwise, original authors will be used or created."
+COM_CMSMIGRATOR_FIELD_IMPORT_AS_SUPER_USER_LABEL="Import All Articles as Current Super User"
+COM_CMSMIGRATOR_FIELD_IMPORT_FILE_LABEL="Import File"
+COM_CMSMIGRATOR_FIELD_MEDIA_CUSTOM_DIR_DESC="Enter the directory name to store images in (e.g., myimages). Will be stored in images/[your_name]"
+COM_CMSMIGRATOR_FIELD_MEDIA_CUSTOM_DIR_LABEL="Custom Directory Name"
+COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_CUSTOM="Custom Directory"
+COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_DESC="Choose where to store migrated images. Default is images/imports. Choose custom to specify a directory."
+COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_LABEL="Media Storage Location"
+COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_ROOT="Default (images/imports)"
+COM_CMSMIGRATOR_FIELD_MEDIA_ZIP_FILE_DESC="Upload a ZIP file containing your WordPress wp-content/uploads folder structure"
+COM_CMSMIGRATOR_FIELD_MEDIA_ZIP_FILE_LABEL="WordPress Uploads ZIP File"
+COM_CMSMIGRATOR_FIELD_SOURCE_CMS_LABEL="Source CMS"
+COM_CMSMIGRATOR_FIELD_SOURCE_CMS_SELECT="Select Source"
+COM_CMSMIGRATOR_FIELD_SOURCE_URL_DESC="Enter the full URL of the original website (e.g., https://my-wordpress-site.com). This is needed to download and import images."
+COM_CMSMIGRATOR_FIELD_SOURCE_URL_LABEL="Source Site URL (for images)"
+COM_CMSMIGRATOR_FORM_NOT_FOUND="The import form could not be loaded."
COM_CMSMIGRATOR_IMPORT_ARTICLES_COUNT="Articles imported: %d"
-COM_CMSMIGRATOR_IMPORT_TAXONOMIES_COUNT="Categories imported: %d"
+COM_CMSMIGRATOR_IMPORT_BUTTON="Import"
+COM_CMSMIGRATOR_IMPORT_ERROR="Import failed: %s"
+COM_CMSMIGRATOR_IMPORT_FAILED="Import failed - no data was processed."
+COM_CMSMIGRATOR_IMPORT_FILE_ERROR="Error uploading import file."
COM_CMSMIGRATOR_IMPORT_MEDIA_COUNT="Media files imported: %d"
+COM_CMSMIGRATOR_IMPORT_PARTIAL="Import completed with some errors:"
COM_CMSMIGRATOR_IMPORT_SKIPPED_COUNT="Articles skipped (already exist): %d"
+COM_CMSMIGRATOR_IMPORT_SUCCESS="Import completed successfully!"
+COM_CMSMIGRATOR_IMPORT_TAXONOMIES_COUNT="Categories imported: %d"
+COM_CMSMIGRATOR_IMPORT_USERS_COUNT="Users imported: %d"
+COM_CMSMIGRATOR_INVALID_FILE="The uploaded file is invalid or could not be read."
+COM_CMSMIGRATOR_INVALID_ITEMLIST_FORMAT="The provided data is not a valid ItemList format."
COM_CMSMIGRATOR_INVALID_JSON_FILE="The uploaded file is not a valid JSON file."
-COM_CMSMIGRATOR_NO_PLUGIN_FOUND="No suitable migration plugin found for %s."
COM_CMSMIGRATOR_INVALID_JSON_FORMAT_FROM_PLUGIN="The plugin returned an invalid JSON format."
-COM_CMSMIGRATOR_INVALID_ITEMLIST_FORMAT="The provided data is not a valid ItemList format."
-COM_CMSMIGRATOR_UNCATEGORISED_NOT_FOUND="The default 'Uncategorised' category was not found."
-COM_CMSMIGRATOR_ARTICLE_IMPORTED_SUCCESS="Article '%s' imported successfully."
-COM_CMSMIGRATOR_FORM_NOT_FOUND="The import form could not be loaded."
-COM_CMSMIGRATOR_MANAGER_CPANEL="CMS Migration Tool"
-COM_CMSMIGRATOR_IMPORT_BUTTON="Import"
-COM_CMSMIGRATOR_UNABLE_TO_LOAD_ARTICLE_TABLE="Could not load article table."
-COM_CMSMIGRATOR_UNABLE_TO_LOAD_CATEGORY_TABLE="Could not load category table."
-COM_CMSMIGRATOR_IMPORT_PARTIAL="Import completed with some errors:"
-COM_CMSMIGRATOR_IMPORT_ERROR="Import failed: %s"
-
-COM_CMSMIGRATOR_JSON_SAVED="Imported JSON data saved to %s"
+COM_CMSMIGRATOR_EMPTY_JSON_FILE="The uploaded JSON file is empty or contains no data."
COM_CMSMIGRATOR_JSON_SAVE_FAILED="Failed to save imported JSON data: %s"
-
-
-COM_CMSMIGRATOR_MIGRATING_DATA="Migration in Progress"
-COM_CMSMIGRATOR_MIGRATING_DATA_DESC="Please wait while we import your data. This may take a few moments..."
-
-; Media Migration
-COM_CMSMIGRATOR_MIGRATION_SETUP="Migration Setup"
-COM_CMSMIGRATOR_MEDIA_MIGRATION_SETTINGS="Media Migration Settings"
-COM_CMSMIGRATOR_MEDIA_MIGRATION_DESC="Configure FTP/FTPS/SFTP settings to automatically download and migrate images from your WordPress site."
-COM_CMSMIGRATOR_MEDIA_INFO_TITLE="Media Migration Information"
-COM_CMSMIGRATOR_MEDIA_INFO_RESOLUTION="Images will be resized to fit within 768Γ512 pixels while maintaining aspect ratio"
+COM_CMSMIGRATOR_JSON_SAVED="Imported JSON data saved to %s"
+COM_CMSMIGRATOR_MANAGER_CPANEL="CMS Migration Tool"
+COM_CMSMIGRATOR_MEDIA_CONNECTION_FAILED="Could not connect to server. Please check your connection credentials."
+COM_CMSMIGRATOR_MEDIA_CONNECTION_FIELDS_REQUIRED="Please fill in all connection fields when media migration is enabled."
+COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR="Custom directory name"
+COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR_DESC="Enter the directory name to store images in (e.g., myimages). Will be stored in images/[your_name]"
+COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR_REQUIRED="Custom directory name is required when custom storage mode is selected."
+COM_CMSMIGRATOR_MEDIA_DOWNLOAD_FAILED="Failed to download media file: %s"
+COM_CMSMIGRATOR_MEDIA_DOWNLOAD_SUCCESS="Media file downloaded successfully: %s"
+COM_CMSMIGRATOR_MEDIA_FTP_CONNECTION_FAILED="Could not connect to FTP server. Please check your FTP credentials."
+COM_CMSMIGRATOR_MEDIA_FTP_FIELDS_REQUIRED="Please fill in all FTP fields when media migration is enabled."
COM_CMSMIGRATOR_MEDIA_INFO_FORMATS="Supports JPG, PNG, GIF, and WebP image formats"
-COM_CMSMIGRATOR_MEDIA_INFO_LOCATION="Downloaded images will be stored in /media/com_cmsmigrator/images/"
COM_CMSMIGRATOR_MEDIA_INFO_FTP="Requires FTP, FTPS, or SFTP access to your WordPress server's wp-content/uploads/ directory"
+COM_CMSMIGRATOR_MEDIA_INFO_LOCATION="Downloaded images will be stored in /media/com_cmsmigrator/images/"
+COM_CMSMIGRATOR_MEDIA_INFO_RESOLUTION="Images will be resized to fit within 768Γ512 pixels while maintaining aspect ratio"
+COM_CMSMIGRATOR_MEDIA_INFO_TITLE="Media Migration Information"
COM_CMSMIGRATOR_MEDIA_INFO_ZIP="Or upload a ZIP file containing your WordPress wp-content/uploads folder structure"
-COM_CMSMIGRATOR_MEDIA_CONNECTION_FIELDS_REQUIRED="Please fill in all connection fields when media migration is enabled."
-COM_CMSMIGRATOR_MEDIA_FTP_FIELDS_REQUIRED="Please fill in all FTP fields when media migration is enabled."
-COM_CMSMIGRATOR_MEDIA_CONNECTION_FAILED="Could not connect to server. Please check your connection credentials."
-COM_CMSMIGRATOR_MEDIA_FTP_CONNECTION_FAILED="Could not connect to FTP server. Please check your FTP credentials."
-COM_CMSMIGRATOR_MEDIA_DOWNLOAD_SUCCESS="Media file downloaded successfully: %s"
-COM_CMSMIGRATOR_MEDIA_DOWNLOAD_FAILED="Failed to download media file: %s"
-COM_CMSMIGRATOR_MEDIA_RESIZE_SUCCESS="Media file resized to fit 768x512: %s"
+COM_CMSMIGRATOR_MEDIA_MIGRATION_DESC="Configure FTP/FTPS/SFTP settings to automatically download and migrate images from your WordPress site."
+COM_CMSMIGRATOR_MEDIA_MIGRATION_SETTINGS="Media Migration Settings"
COM_CMSMIGRATOR_MEDIA_RESIZE_FAILED="Failed to resize media file: %s"
+COM_CMSMIGRATOR_MEDIA_RESIZE_SUCCESS="Media file resized to fit 768x512: %s"
+COM_CMSMIGRATOR_MEDIA_STORAGE_MODE="Media storage location"
+COM_CMSMIGRATOR_MEDIA_STORAGE_MODE_DESC="Choose where to store migrated images. Default is images/imports. Choose custom to specify a directory."
COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION="Test Connection"
-COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS="Connection successful! Connection to %s established."
COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED="Connection failed: %s"
+COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS="Connection successful! Connection to %s established."
COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_TESTING="Testing connection..."
-
-COM_CMSMIGRATOR_MEDIA_STORAGE_MODE="Media storage location"
-COM_CMSMIGRATOR_MEDIA_STORAGE_MODE_DESC="Choose where to store migrated images. Default is images/imports. Choose custom to specify a directory."
-COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR="Custom directory name"
-COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR_DESC="Enter the directory name to store images in (e.g., myimages). Will be stored in images/[your_name]"
-COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR_REQUIRED="Please enter a custom directory name for media storage."
-COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_LABEL="Media Storage Location"
-COM_CMSMIGRATOR_FIELD_MEDIA_STORAGE_MODE_DESC="Choose where to store migrated images. Default is images/imports. Choose custom to specify a directory."
-
-; ZIP Upload Media Migration
+COM_CMSMIGRATOR_MEDIA_ZIP_COMPLETE="ZIP media migration completed successfully."
+COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_ERROR="Error extracting ZIP file: %s"
+COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_FAILED="Failed to extract ZIP file. The file may be corrupted."
+COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACTED_SUCCESS="ZIP file extracted successfully to images directory. %d media files extracted."
COM_CMSMIGRATOR_MEDIA_ZIP_FILE_ERROR="Error uploading media ZIP file."
COM_CMSMIGRATOR_MEDIA_ZIP_FILE_MISSING="Media ZIP file is missing or invalid."
-COM_CMSMIGRATOR_MEDIA_ZIP_UPLOAD_ERROR="Error uploading ZIP file. Please try again."
COM_CMSMIGRATOR_MEDIA_ZIP_INVALID_TYPE="The uploaded file is not a valid ZIP file."
-COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_FAILED="Failed to extract ZIP file. The file may be corrupted."
-COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACTED_SUCCESS="ZIP file extracted successfully to images directory. %d media files extracted."
-COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_ERROR="Error extracting ZIP file: %s"
-COM_CMSMIGRATOR_MEDIA_ZIP_URL_REPLACED="Image URL updated: %s"
COM_CMSMIGRATOR_MEDIA_ZIP_PROCESSING="Processing ZIP file and extracting media files..."
-COM_CMSMIGRATOR_MEDIA_ZIP_COMPLETE="ZIP media migration completed successfully."
-COM_CMSMIGRATOR_MEDIA_CUSTOM_DIR_REQUIRED="Custom directory name is required when custom storage mode is selected."
\ No newline at end of file
+COM_CMSMIGRATOR_MEDIA_ZIP_UPLOAD_ERROR="Error uploading ZIP file. Please try again."
+COM_CMSMIGRATOR_MEDIA_ZIP_URL_REPLACED="Image URL updated: %s"
+COM_CMSMIGRATOR_MENU_BACKEND="Migrate to Joomla"
+COM_CMSMIGRATOR_MIGRATING_DATA="Migration in Progress"
+COM_CMSMIGRATOR_MIGRATING_DATA_DESC="Please wait while we import your data. This may take a few moments..."
+COM_CMSMIGRATOR_MIGRATION_SETUP="Migration Setup"
+COM_CMSMIGRATOR_NO_PLUGIN_FOUND="No suitable migration plugin found for %s."
+COM_CMSMIGRATOR_UNABLE_TO_LOAD_ARTICLE_TABLE="Could not load article table."
+COM_CMSMIGRATOR_UNABLE_TO_LOAD_CATEGORY_TABLE="Could not load category table."
+COM_CMSMIGRATOR_UNCATEGORISED_NOT_FOUND="The default 'Uncategorised' category was not found."
\ No newline at end of file
diff --git a/src/component/admin/language/en-GB/com_cmsmigrator.sys.ini b/src/component/admin/language/en-GB/com_cmsmigrator.sys.ini
index cb11585..9a71bcb 100644
--- a/src/component/admin/language/en-GB/com_cmsmigrator.sys.ini
+++ b/src/component/admin/language/en-GB/com_cmsmigrator.sys.ini
@@ -1,3 +1,10 @@
-COM_CMSMIGRATOR = "Migrate To Joomla"
-COM_CMSMIGRATOR_DESCRIPTION = "This component migrates websites from other platforms to Joomla."
-COM_CMSMIGRATOR_MENU_BACKEND = "Migrate to Joomla"
\ No newline at end of file
+;
+; @package Joomla.Administrator
+; @subpackage com_cmsmigrator
+; @copyright Copyright (C) 2025 Open Source Matters, Inc.
+; @license GNU General Public License version 2 or later; see LICENSE.txt
+;
+
+COM_CMSMIGRATOR="Migrate To Joomla"
+COM_CMSMIGRATOR_DESCRIPTION="This component migrates websites from other platforms to Joomla."
+COM_CMSMIGRATOR_MENU_BACKEND="Migrate to Joomla"
\ No newline at end of file
diff --git a/src/component/admin/services/provider.php b/src/component/admin/services/provider.php
index 34f9d48..779254c 100644
--- a/src/component/admin/services/provider.php
+++ b/src/component/admin/services/provider.php
@@ -1,5 +1,12 @@
registerServiceProvider(new MVCFactory('\\Binary\\Component\\CmsMigrator'));
- $container->registerServiceProvider(new ComponentDispatcherFactory('\\Binary\\Component\\CmsMigrator'));
+ $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\CmsMigrator'));
+ $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\CmsMigrator'));
$container->set(
ComponentInterface::class,
diff --git a/src/component/admin/sql/install.mysql.utf8.sql b/src/component/admin/sql/install.mysql.utf8.sql
index adbbc03..5ffbeac 100644
--- a/src/component/admin/sql/install.mysql.utf8.sql
+++ b/src/component/admin/sql/install.mysql.utf8.sql
@@ -1,3 +1,10 @@
+--
+-- @package Joomla.Administrator
+-- @subpackage com_cmsmigrator
+-- @copyright Copyright (C) 2025 Open Source Matters, Inc.
+-- @license GNU General Public License version 2 or later; see LICENSE.txt
+--
+
CREATE TABLE IF NOT EXISTS `#__cmsmigrator_articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL DEFAULT '',
diff --git a/src/component/admin/sql/install.postgresql.utf8.sql b/src/component/admin/sql/install.postgresql.utf8.sql
new file mode 100644
index 0000000..d84ee89
--- /dev/null
+++ b/src/component/admin/sql/install.postgresql.utf8.sql
@@ -0,0 +1,28 @@
+--
+-- @package Joomla.Administrator
+-- @subpackage com_cmsmigrator
+-- @copyright Copyright (C) 2025 Open Source Matters, Inc.
+-- @license GNU General Public License version 2 or later; see LICENSE.txt
+--
+
+CREATE TABLE IF NOT EXISTS "#__cmsmigrator_articles" (
+ "id" SERIAL NOT NULL,
+ "title" VARCHAR(255) NOT NULL DEFAULT '',
+ "alias" VARCHAR(255) NOT NULL DEFAULT '',
+ "content" TEXT NOT NULL,
+ "state" SMALLINT NOT NULL DEFAULT 0,
+ "catid" INTEGER NOT NULL DEFAULT 0,
+ "created" TIMESTAMP NOT NULL,
+ "created_by" INTEGER NOT NULL DEFAULT 0,
+ "publish_up" TIMESTAMP NULL DEFAULT NULL,
+ "access" INTEGER NOT NULL DEFAULT 1,
+ "language" CHAR(7) NOT NULL DEFAULT '*',
+ "ordering" INTEGER NOT NULL DEFAULT 0,
+ "params" TEXT NOT NULL DEFAULT '{}',
+ PRIMARY KEY ("id")
+);
+
+CREATE INDEX IF NOT EXISTS "idx_cmsmigrator_articles_state" ON "#__cmsmigrator_articles" ("state");
+CREATE INDEX IF NOT EXISTS "idx_cmsmigrator_articles_catid" ON "#__cmsmigrator_articles" ("catid");
+CREATE INDEX IF NOT EXISTS "idx_cmsmigrator_articles_createdby" ON "#__cmsmigrator_articles" ("created_by");
+CREATE INDEX IF NOT EXISTS "idx_cmsmigrator_articles_language" ON "#__cmsmigrator_articles" ("language");
\ No newline at end of file
diff --git a/src/component/admin/src/Controller/DisplayController.php b/src/component/admin/src/Controller/DisplayController.php
index fed7838..25b63bd 100644
--- a/src/component/admin/src/Controller/DisplayController.php
+++ b/src/component/admin/src/Controller/DisplayController.php
@@ -1,6 +1,13 @@
checkToken();
-
+
// Reset progress file at the start of new migration
$progressFile = JPATH_SITE . '/media/com_cmsmigrator/imports/progress.json';
if (file_exists($progressFile)) {
@@ -30,7 +34,7 @@ public function import()
'status' => 'Starting migration...',
'timestamp' => time()
]));
-
+
// Retrieves form data (jform) and uploaded files.
$app = $this->app;
$input = $this->input;
@@ -40,10 +44,10 @@ public function import()
$file = $files['import_file'] ?? null;
$sourceCms = $jform['source_cms'] ?? null;
$sourceUrl = $jform['source_url'] ?? null;
-
+
// Get media ZIP file if ZIP upload is selected
$mediaZipFile = $files['media_zip_file'] ?? null;
-
+
// Get connection configuration for media migration
$connectionConfig = [];
$mediaStorageMode = $jform['media_storage_mode'] ?? 'root';
@@ -55,9 +59,9 @@ public function import()
$this->setRedirect('index.php?option=com_cmsmigrator');
return;
}
-
+
$connectionType = $jform['connection_type'] ?? 'ftp';
-
+
if ($connectionType === 'zip') {
// Handle ZIP upload validation
if (empty($mediaZipFile) || $mediaZipFile['error'] !== UPLOAD_ERR_OK) {
@@ -65,7 +69,7 @@ public function import()
$this->setRedirect('index.php?option=com_cmsmigrator');
return;
}
-
+
$connectionConfig = [
'connection_type' => 'zip',
'zip_file' => $mediaZipFile,
@@ -86,7 +90,7 @@ public function import()
];
}
}
-
+
//Ensures a file was uploaded and it was successful
if (empty($file) || $file['error'] !== UPLOAD_ERR_OK) {
$app->enqueueMessage(Text::_('COM_CMSMIGRATOR_IMPORT_FILE_ERROR'), 'error');
@@ -96,24 +100,11 @@ public function import()
$importAsSuperUser = !empty($jform['import_as_super_user']) && $jform['import_as_super_user'] == '1';
//Passes the data to ImportModel Function
$model = $this->getModel('Import');
- try {
- $mvcFactory = $this->getMVCFactory();
- $model->setMVCFactory($mvcFactory);
- } catch (\Exception $e) {
- // MVC factory not available on controller, try to get it from the application/component
- try {
- $app = Factory::getApplication();
- $component = $app->bootComponent('com_cmsmigrator');
- if (method_exists($component, 'getMVCFactory')) {
- $model->setMVCFactory($component->getMVCFactory());
- }
- } catch (\Exception $componentException) {
- // No MVC factory available, model will work with fallback
- }
- }
+ $mvcFactory = $this->factory;
+ $model->setMVCFactory($mvcFactory);
try {
if (!$model->import($file, $sourceCms, $sourceUrl, $connectionConfig, $importAsSuperUser)) {
- $app->enqueueMessage('Import failed', 'error');
+ $app->enqueueMessage(Text::_('COM_CMSMIGRATOR_IMPORT_FAILED'), 'error');
$this->setRedirect('index.php?option=com_cmsmigrator');
return;
}
@@ -136,10 +127,10 @@ public function testConnection()
{
// Check for request forgeries
$this->checkToken();
-
+
$app = $this->app;
$input = $this->input;
-
+
// Get connection configuration
$connectionConfig = [
'connection_type' => $input->getString('connection_type', 'ftp'),
@@ -149,11 +140,11 @@ public function testConnection()
'password' => $input->getString('password', ''),
'passive' => $input->getBool('passive', true)
];
-
+
// Test connection using controller's model helper to avoid mvcFactory issues
$mediaModel = $this->getModel('Media', 'Administrator', ['ignore_request' => true]);
$result = $mediaModel ? $mediaModel->testConnection($connectionConfig) : ['success' => false, 'message' => 'Could not create Media model'];
-
+
// Send JSON response
$app->setHeader('Content-Type', 'application/json');
$app->sendHeaders();
@@ -170,10 +161,10 @@ public function testFtp()
{
// Check for request forgeries
$this->checkToken();
-
+
$app = $this->app;
$input = $this->input;
-
+
// Get FTP configuration
$ftpConfig = [
'connection_type' => 'ftp',
@@ -183,11 +174,11 @@ public function testFtp()
'password' => $input->getString('password', ''),
'passive' => $input->getBool('passive', true)
];
-
+
// Test connection
$mediaModel = $this->getModel('Media', 'Administrator');
$result = $mediaModel ? $mediaModel->testConnection($ftpConfig) : ['success' => false, 'message' => 'Could not create Media model'];
-
+
// Send JSON response
$app->setHeader('Content-Type', 'application/json');
$app->sendHeaders();
@@ -207,4 +198,4 @@ public function progress()
}
exit;
}
-}
\ No newline at end of file
+}
diff --git a/src/component/admin/src/Event/MigrationEvent.php b/src/component/admin/src/Event/MigrationEvent.php
index 219f743..7164e46 100644
--- a/src/component/admin/src/Event/MigrationEvent.php
+++ b/src/component/admin/src/Event/MigrationEvent.php
@@ -1,6 +1,13 @@
results ?? [];
}
-}
\ No newline at end of file
+}
diff --git a/src/component/admin/src/Extension/CmsMigratorComponent.php b/src/component/admin/src/Extension/CmsMigratorComponent.php
index a16aefc..936c12e 100644
--- a/src/component/admin/src/Extension/CmsMigratorComponent.php
+++ b/src/component/admin/src/Extension/CmsMigratorComponent.php
@@ -1,6 +1,13 @@
app->enqueueMessage(Text::sprintf('COM_CMSMIGRATOR_JSON_SAVED', $filePath), 'message');
- } catch (\Exception $e) {
- $this->app->enqueueMessage(Text::sprintf('COM_CMSMIGRATOR_JSON_SAVE_FAILED', $e->getMessage()), 'error');
- }
- }
+ // if ($convertedData) {
+ // $importPath = JPATH_SITE . '/media/com_cmsmigrator/imports';
+ // Folder::create($importPath);
+
+ // $fileName = 'import_' . $sourceCms . '_' . time() . '.json';
+ // $filePath = $importPath . '/' . $fileName;
+
+ // try {
+ // File::write($filePath, $convertedData);
+ // $this->app->enqueueMessage(Text::sprintf('COM_CMSMIGRATOR_JSON_SAVED', $filePath), 'message');
+ // } catch (\Exception $e) {
+ // $this->app->enqueueMessage(Text::sprintf('COM_CMSMIGRATOR_JSON_SAVE_FAILED', $e->getMessage()), 'error');
+ // }
+ // }
if (!$convertedData) {
throw new \RuntimeException(Text::sprintf('COM_CMSMIGRATOR_NO_PLUGIN_FOUND', $sourceCms));
@@ -97,12 +104,12 @@ public function import($file, $sourceCms, $sourceUrl = '', $ftpConfig = [], $imp
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException(Text::_('COM_CMSMIGRATOR_INVALID_JSON_FORMAT_FROM_PLUGIN'));
}
-
+
try {
$processor = $this->getMVCFactory()->createModel('Processor', 'Administrator', ['ignore_request' => true]);
//Processor function to process data to Joomla Tables
$result = $processor->process($data, $sourceUrl, $ftpConfig, $importAsSuperUser);
-
+
if ($result['success']) {
$message = Text::_('COM_CMSMIGRATOR_IMPORT_SUCCESS') . '
' .
Text::sprintf('COM_CMSMIGRATOR_IMPORT_USERS_COUNT', $result['counts']['users']) . '
' .
@@ -129,4 +136,4 @@ public function import($file, $sourceCms, $sourceUrl = '', $ftpConfig = [], $imp
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/component/admin/src/Model/MediaModel.php b/src/component/admin/src/Model/MediaModel.php
index 7ce13cc..09cad58 100644
--- a/src/component/admin/src/Model/MediaModel.php
+++ b/src/component/admin/src/Model/MediaModel.php
@@ -7,7 +7,7 @@
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
-namespace Binary\Component\CmsMigrator\Administrator\Model;
+namespace Joomla\Component\CmsMigrator\Administrator\Model;
\defined('_JEXEC') or die;
@@ -132,8 +132,8 @@ public function __construct($config = [])
/**
* Sets the storage directory.
- *
- * The storage directory will contain the WordPress media files organized
+ *
+ * The storage directory will contain the WordPress media files organized
* in their original folder structure (e.g., 2024/01/image.jpg).
*
* @param string $dir The directory name (e.g., 'imports', 'custom', etc.).
@@ -268,7 +268,9 @@ protected function extractImageUrls(string $content): array
if (!empty($matches[1])) {
foreach ($matches[1] as $url) {
- if (strpos($url, 'data:') === 0) continue;
+ if (strpos($url, 'data:') === 0) {
+ continue;
+ }
$imageUrls[] = $url;
}
}
@@ -278,7 +280,7 @@ protected function extractImageUrls(string $content): array
if (!empty($wpMatches[0])) {
$imageUrls = array_merge($imageUrls, $wpMatches[0]);
}
-
+
// Match direct uploads folder URLs
preg_match_all('/https?:\/\/[^\/]+\/uploads\/[^\s"\'<>]+\.(jpg|jpeg|png|gif|webp)/i', $content, $directMatches);
if (!empty($directMatches[0])) {
@@ -306,11 +308,11 @@ protected function downloadAndProcessImage(string $imageUrl): ?string
}
$uploadPath = $parsedUrl['path'];
-
+
// Check for different WordPress upload path patterns
$isWordPressUpload = false;
$relativePath = '';
-
+
if (strpos($uploadPath, '/wp-content/uploads/') !== false) {
// Standard WordPress structure: /wp-content/uploads/...
$isWordPressUpload = true;
@@ -322,7 +324,7 @@ protected function downloadAndProcessImage(string $imageUrl): ?string
preg_match('/.*\/uploads\/(.+)$/', $uploadPath, $matches);
$relativePath = $matches[1] ?? '';
}
-
+
if (!$isWordPressUpload || empty($relativePath)) {
$this->app->enqueueMessage("Not a WordPress upload path: $uploadPath", 'warning');
return null;
@@ -423,7 +425,7 @@ protected function generateCandidateRemotePaths(string $resizedPath, string $ori
{
$candidatePaths = [];
$documentRoot = $this->documentRoot === '.' ? '' : $this->documentRoot;
-
+
// Try different WordPress structure patterns
$structures = [
// Standard structure: {documentRoot}/wp-content/uploads/...
@@ -435,14 +437,14 @@ protected function generateCandidateRemotePaths(string $resizedPath, string $ori
// Root uploads: uploads/...
'uploads/'
];
-
+
foreach ($structures as $structure) {
// Add resized version first (usually smaller file)
$candidatePaths[] = $structure . $resizedPath;
// Add original version
$candidatePaths[] = $structure . $originalPath;
}
-
+
return array_unique($candidatePaths);
}
@@ -521,7 +523,7 @@ protected function downloadFileViaSftp(string $remotePath, string $localPath): b
try {
$result = $this->sftpConnection->get($remotePath, $localPath);
-
+
if ($result !== false) {
return true;
}
@@ -549,31 +551,31 @@ protected function getLocalFileName(string $remotePath): string
// Return the WordPress uploads structure (e.g., 2024/01/image.jpg)
return $matches[1];
}
-
+
// Extract the path after direct uploads/
$pattern = '/.*\/uploads\/(.+)$/';
if (preg_match($pattern, $remotePath, $matches)) {
// Return the uploads structure (e.g., 2024/01/image.jpg)
return $matches[1];
}
-
+
// Fallback: clean the path but preserve some structure
$cleanPath = $remotePath;
-
+
// Remove document root if present
if ($this->documentRoot !== '.' && strpos($cleanPath, $this->documentRoot . '/') === 0) {
$cleanPath = substr($cleanPath, strlen($this->documentRoot . '/'));
}
-
+
// Remove wp-content/uploads/ or uploads/ prefix
$cleanPath = preg_replace('/^(wp-content\/)?uploads\//', '', $cleanPath);
-
+
// Sanitize directory and file names separately to preserve folder structure
$pathParts = explode('/', $cleanPath);
- $sanitizedParts = array_map(function($part) {
+ $sanitizedParts = array_map(function ($part) {
return preg_replace('/[^a-zA-Z0-9._-]/', '_', $part);
}, $pathParts);
-
+
return implode('/', $sanitizedParts);
}
@@ -608,19 +610,19 @@ protected function autoDetectDocumentRoot(): void
foreach ($testScenarios as [$root, $contentPath]) {
$canAccess = false;
$hasWordPressContent = false;
-
+
if ($this->connectionType === 'sftp' && $this->sftpConnection) {
try {
$checkPath = $root ? $root . '/' . $contentPath : $contentPath;
-
+
// Check if the root directory exists (or skip if empty root)
if (empty($root) || $this->sftpConnection->is_dir($root)) {
$canAccess = true;
-
+
// Check for WordPress content structure
if ($this->sftpConnection->is_dir($checkPath)) {
$hasWordPressContent = true;
-
+
// For wp-content, also check for uploads subdirectory
if ($contentPath === 'wp-content') {
$uploadsPath = $checkPath . '/uploads';
@@ -633,17 +635,17 @@ protected function autoDetectDocumentRoot(): void
} catch (\Exception $e) {
// Continue to next scenario
}
- } else if ($this->ftpConnection) {
+ } elseif ($this->ftpConnection) {
$originalDir = @ftp_pwd($this->ftpConnection);
-
+
// Check if we can access the root directory (or skip if empty root)
if (empty($root) || @ftp_chdir($this->ftpConnection, $root)) {
$canAccess = true;
-
+
// Check for WordPress content structure
if (@ftp_chdir($this->ftpConnection, $contentPath)) {
$hasWordPressContent = true;
-
+
// For wp-content, also check for uploads subdirectory
if ($contentPath === 'wp-content') {
if (@ftp_chdir($this->ftpConnection, 'uploads')) {
@@ -652,7 +654,7 @@ protected function autoDetectDocumentRoot(): void
@ftp_chdir($this->ftpConnection, '..');
}
}
-
+
// Return to root
@ftp_chdir($this->ftpConnection, $originalDir ?: '/');
} else {
@@ -664,21 +666,21 @@ protected function autoDetectDocumentRoot(): void
@ftp_chdir($this->ftpConnection, $originalDir ?: '/');
}
}
-
+
if ($canAccess && $hasWordPressContent) {
$this->documentRoot = $root ?: '.';
$this->documentRootDetected = true;
-
+
$detectedStructure = $root ? "{$root}/{$contentPath}" : $contentPath;
$this->app->enqueueMessage(
"β
WordPress structure auto-detected: {$detectedStructure} (Document root: {$this->documentRoot})",
'info'
);
-
+
return;
}
}
-
+
// If no valid structure found, use default and mark as detected to avoid repeated attempts
$this->documentRootDetected = true;
$this->app->enqueueMessage(
@@ -699,7 +701,7 @@ protected function autoDetectDocumentRoot(): void
public function connect(array $config): bool
{
$this->connectionType = $config['connection_type'] ?? 'ftp';
-
+
if ($this->connectionType === 'zip') {
return $this->processZipUpload($config);
} elseif ($this->connectionType === 'sftp') {
@@ -777,7 +779,7 @@ protected function connectFtps(array $config): bool
// Use ftp_ssl_connect for FTPS (FTP over SSL/TLS)
$this->ftpConnection = ftp_ssl_connect($config['host'], $config['port'] ?? 21, 15);
-
+
if (!$this->ftpConnection) {
$this->app->enqueueMessage("Failed to connect to FTPS server: {$config['host']}", 'error');
return false;
@@ -823,7 +825,7 @@ protected function connectSftp(array $config): bool
try {
$this->sftpConnection = new SFTP($config['host'], $config['port'] ?? 22);
-
+
if (!$this->sftpConnection->login($config['username'], $config['password'])) {
$this->app->enqueueMessage('SFTP login failed', 'error');
$this->sftpConnection = null;
@@ -855,7 +857,7 @@ protected function processZipUpload(array $config): bool
}
$zipFile = $config['zip_file'];
-
+
// Validate file upload
if ($zipFile['error'] !== UPLOAD_ERR_OK) {
$this->app->enqueueMessage(Text::_('COM_CMSMIGRATOR_MEDIA_ZIP_UPLOAD_ERROR'), 'error');
@@ -875,7 +877,7 @@ protected function processZipUpload(array $config): bool
try {
// Extract ZIP file to storage directory
$extractPath = $this->mediaBasePath;
-
+
// Ensure extraction directory exists
if (!is_dir($extractPath)) {
Folder::create($extractPath);
@@ -886,8 +888,8 @@ protected function processZipUpload(array $config): bool
// Extract ZIP
$zip = new \ZipArchive();
$result = $zip->open($zipFile['tmp_name']);
-
- if ($result !== TRUE) {
+
+ if ($result !== true) {
$this->app->enqueueMessage(Text::_('COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_FAILED'), 'error');
return false;
}
@@ -895,64 +897,63 @@ protected function processZipUpload(array $config): bool
$extractedFiles = 0;
$totalFiles = $zip->numFiles;
$processedFiles = 0;
-
+
// Extract files with progress tracking
for ($i = 0; $i < $totalFiles; $i++) {
$filename = $zip->getNameIndex($i);
$processedFiles++;
-
+
// Report progress every 10 files or at the end
if ($processedFiles % 10 === 0 || $processedFiles === $totalFiles) {
$progressPercent = min(15, (int)(($processedFiles / $totalFiles) * 15)); // ZIP processing takes up to 15% of total progress
$this->updateZipProgress($progressPercent, sprintf('Extracting files from ZIP: %d/%d', $processedFiles, $totalFiles));
}
-
+
// Skip directories and hidden files
if (substr($filename, -1) === '/' || strpos(basename($filename), '.') === 0) {
continue;
}
-
+
// Skip non-media files - only extract common media file types
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'mp4', 'mp3', 'zip'];
if (!in_array($extension, $allowedExtensions)) {
continue;
}
-
+
// Extract individual file to handle path structure better
$fileData = $zip->getFromIndex($i);
if ($fileData !== false) {
// Handle WordPress uploads folder structure
$relativePath = $this->normalizeUploadPath($filename);
$targetPath = $extractPath . $relativePath;
-
+
// Ensure target directory exists
$targetDir = dirname($targetPath);
if (!is_dir($targetDir)) {
Folder::create($targetDir);
}
-
+
// Write the file
if (File::write($targetPath, $fileData)) {
$extractedFiles++;
}
}
}
-
+
$zip->close();
$this->app->enqueueMessage(
- Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACTED_SUCCESS', $extractedFiles),
+ Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACTED_SUCCESS', $extractedFiles),
'message'
);
-
+
$this->app->enqueueMessage(Text::_('COM_CMSMIGRATOR_MEDIA_ZIP_COMPLETE'), 'info');
-
- return true;
+ return true;
} catch (\Exception $e) {
$this->app->enqueueMessage(
- Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_ERROR', $e->getMessage()),
+ Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_EXTRACT_ERROR', $e->getMessage()),
'error'
);
return false;
@@ -981,7 +982,7 @@ protected function processContentForZipUpload(string $content, array $imageUrls)
if ($newUrl) {
$updatedContent = str_replace($originalUrl, $newUrl, $updatedContent);
$this->app->enqueueMessage(
- Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_URL_REPLACED', basename($originalUrl)),
+ Text::sprintf('COM_CMSMIGRATOR_MEDIA_ZIP_URL_REPLACED', basename($originalUrl)),
'info'
);
}
@@ -1025,7 +1026,7 @@ protected function findExtractedImageUrl(string $originalUrl): ?string
if (preg_match('/.*\/wp-content\/uploads\/(.+)$/i', $urlPath, $matches)) {
$relativePath = $matches[1];
}
- // Pattern 2: /uploads/2024/01/image.jpg
+ // Pattern 2: /uploads/2024/01/image.jpg
elseif (preg_match('/.*\/uploads\/(.+)$/i', $urlPath, $matches)) {
$relativePath = $matches[1];
}
@@ -1131,7 +1132,7 @@ public function getPlannedJoomlaUrl(string $wordpressUrl): ?string
}
$uploadPath = $parsedUrl['path'];
-
+
// Check for standard WordPress structure: /wp-content/uploads/
if (strpos($uploadPath, '/wp-content/uploads/') !== false) {
$pattern = '/.*\/wp-content\/uploads\/(.+)$/';
@@ -1139,7 +1140,7 @@ public function getPlannedJoomlaUrl(string $wordpressUrl): ?string
return $this->mediaBaseUrl . $matches[1];
}
}
-
+
// Check for direct uploads structure: /uploads/
if (strpos($uploadPath, '/uploads/') !== false) {
$pattern = '/.*\/uploads\/(.+)$/';
@@ -1210,7 +1211,9 @@ public function batchDownloadMedia(array $mediaUrls, array $config): array
$this->processBatchDownload($batch, $results);
}
- $successCount = count(array_filter($results, function($result) { return $result['success']; }));
+ $successCount = count(array_filter($results, function ($result) {
+ return $result['success'];
+ }));
$this->app->enqueueMessage(
sprintf('β
Batch download complete: %d/%d files downloaded successfully', $successCount, count($results)),
'info'
@@ -1232,7 +1235,7 @@ protected function batchProcessZipMedia(array $mediaUrls): array
{
$results = [];
$foundCount = 0;
-
+
$this->app->enqueueMessage(
sprintf('Processing %d media URLs from extracted ZIP files...', count($mediaUrls)),
'info'
@@ -1240,7 +1243,7 @@ protected function batchProcessZipMedia(array $mediaUrls): array
foreach ($mediaUrls as $originalUrl) {
$localUrl = $this->findExtractedImageUrl($originalUrl);
-
+
if ($localUrl) {
$results[$originalUrl] = [
'success' => true,
@@ -1256,7 +1259,7 @@ protected function batchProcessZipMedia(array $mediaUrls): array
];
}
}
-
+
$this->app->enqueueMessage(
sprintf('β
ZIP media processing complete: %d/%d files found in extracted content', $foundCount, count($mediaUrls)),
'info'
@@ -1282,11 +1285,11 @@ protected function prepareDownloadPaths(string $imageUrl): array
}
$uploadPath = $parsedUrl['path'];
-
+
// Check for different WordPress upload path patterns
$isWordPressUpload = false;
$relativePath = '';
-
+
if (strpos($uploadPath, '/wp-content/uploads/') !== false) {
// Standard WordPress structure: /wp-content/uploads/...
$isWordPressUpload = true;
@@ -1298,7 +1301,7 @@ protected function prepareDownloadPaths(string $imageUrl): array
preg_match('/.*\/uploads\/(.+)$/', $uploadPath, $matches);
$relativePath = $matches[1] ?? '';
}
-
+
if (!$isWordPressUpload || empty($relativePath)) {
return [];
}
@@ -1324,10 +1327,10 @@ protected function prepareDownloadPaths(string $imageUrl): array
}
$paths = [];
-
+
// Generate candidate remote paths based on detected structure
$candidatePaths = $this->generateCandidateRemotePaths($resizedPath, $originalPath);
-
+
foreach ($candidatePaths as $remotePath) {
$paths[] = [
'remote' => $remotePath,
@@ -1353,7 +1356,7 @@ protected function processBatchDownload(array $downloadTasks, array &$results):
{
foreach ($downloadTasks as $imageUrl => $paths) {
$downloaded = false;
-
+
foreach ($paths as $pathInfo) {
$localDir = dirname($pathInfo['local']);
if (!is_dir($localDir)) {
@@ -1400,7 +1403,7 @@ public function getMediaStats(): array
* Test FTP, FTPS, or SFTP connection and auto-detect document root
*
* @param array $config The connection configuration
- *
+ *
* @return array Result containing success status and message
*
* @since 1.0.0
@@ -1433,7 +1436,7 @@ public function testConnection(array $config): array
* Test FTP connection and auto-detect document root
*
* @param array $config The FTP configuration
- *
+ *
* @return array Result containing success status and message
*
* @since 1.0.0
@@ -1447,7 +1450,7 @@ protected function testFtpConnection(array $config): array
// Try to connect
$connection = @ftp_connect($config['host'], $config['port'] ?? 21, 15);
-
+
if (!$connection) {
$result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED', 'Could not connect to FTP server');
return $result;
@@ -1455,7 +1458,7 @@ protected function testFtpConnection(array $config): array
// Try to login
$loginResult = @ftp_login($connection, $config['username'], $config['password']);
-
+
if (!$loginResult) {
ftp_close($connection);
$result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED', 'Invalid FTP credentials');
@@ -1472,14 +1475,14 @@ protected function testFtpConnection(array $config): array
// Close the connection
ftp_close($connection);
-
+
// Return success with detected root info
$result['success'] = true;
if ($detectedRoot) {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
"
Document root detected: \"{$detectedRoot}\" with WordPress content.";
} else {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
'
Warning: Could not detect document root with WordPress content.';
}
@@ -1490,7 +1493,7 @@ protected function testFtpConnection(array $config): array
* Test FTPS connection and auto-detect document root
*
* @param array $config The FTPS configuration
- *
+ *
* @return array Result containing success status and message
*
* @since 1.0.0
@@ -1504,7 +1507,7 @@ protected function testFtpsConnection(array $config): array
// Try to connect using FTPS (FTP over SSL/TLS)
$connection = @ftp_ssl_connect($config['host'], $config['port'] ?? 21, 15);
-
+
if (!$connection) {
$result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED', 'Could not connect to FTPS server');
return $result;
@@ -1512,7 +1515,7 @@ protected function testFtpsConnection(array $config): array
// Try to login
$loginResult = @ftp_login($connection, $config['username'], $config['password']);
-
+
if (!$loginResult) {
ftp_close($connection);
$result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED', 'Invalid FTPS credentials');
@@ -1529,14 +1532,14 @@ protected function testFtpsConnection(array $config): array
// Close the connection
ftp_close($connection);
-
+
// Return success with detected root info
$result['success'] = true;
if ($detectedRoot) {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
"
Document root detected: \"{$detectedRoot}\" with WordPress content.";
} else {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
'
Warning: Could not detect document root with WordPress content.';
}
@@ -1547,7 +1550,7 @@ protected function testFtpsConnection(array $config): array
* Test SFTP connection and auto-detect document root
*
* @param array $config The SFTP configuration
- *
+ *
* @return array Result containing success status and message
*
* @since 1.0.0
@@ -1561,7 +1564,7 @@ protected function testSftpConnection(array $config): array
try {
$sftp = new SFTP($config['host'], $config['port'] ?? 22);
-
+
if (!$sftp->login($config['username'], $config['password'])) {
$result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_FAILED', 'Invalid SFTP credentials');
return $result;
@@ -1572,14 +1575,14 @@ protected function testSftpConnection(array $config): array
// Disconnect
$sftp->disconnect();
-
+
// Return success with detected root info
$result['success'] = true;
if ($detectedRoot) {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
"
Document root detected: \"{$detectedRoot}\" with WordPress content.";
} else {
- $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
+ $result['message'] = Text::sprintf('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS', $config['host']) .
'
Warning: Could not detect document root with WordPress content.';
}
@@ -1594,7 +1597,7 @@ protected function testSftpConnection(array $config): array
* Detect document root via FTP
*
* @param resource $connection The FTP connection
- *
+ *
* @return string|null The detected document root or null
*
* @since 1.0.0
@@ -1602,7 +1605,7 @@ protected function testSftpConnection(array $config): array
protected function detectDocumentRootFtp($connection): ?string
{
$commonRoots = ['httpdocs', 'public_html', 'www'];
-
+
foreach ($commonRoots as $root) {
if (@ftp_chdir($connection, $root)) {
// Try to find wp-content directory to confirm this is the right root
@@ -1620,7 +1623,7 @@ protected function detectDocumentRootFtp($connection): ?string
@ftp_chdir($connection, '/');
return '.';
}
-
+
// Check for direct uploads folder in root
if (@ftp_chdir($connection, 'uploads')) {
@ftp_chdir($connection, '/');
@@ -1633,7 +1636,7 @@ protected function detectDocumentRootFtp($connection): ?string
* Detect document root via SFTP
*
* @param SFTP $sftp The SFTP connection
- *
+ *
* @return string|null The detected document root or null
*
* @since 1.0.0
@@ -1641,7 +1644,7 @@ protected function detectDocumentRootFtp($connection): ?string
protected function detectDocumentRootSftp(SFTP $sftp): ?string
{
$commonRoots = ['httpdocs', 'public_html', 'www'];
-
+
foreach ($commonRoots as $root) {
try {
// Check if directory exists and has wp-content
@@ -1660,7 +1663,7 @@ protected function detectDocumentRootSftp(SFTP $sftp): ?string
} catch (\Exception $e) {
// Continue to next check
}
-
+
// Check for direct uploads folder in root
try {
if ($sftp->is_dir('uploads')) {
@@ -1669,7 +1672,7 @@ protected function detectDocumentRootSftp(SFTP $sftp): ?string
} catch (\Exception $e) {
// Continue
}
-
+
return null;
}
@@ -1703,48 +1706,48 @@ protected function normalizeUploadPath(string $zipPath): string
{
// Clean up the path
$zipPath = str_replace('\\', '/', $zipPath);
-
+
// Handle different possible ZIP structures:
// 1. wp-content/uploads/2024/01/image.jpg
- // 2. uploads/2024/01/image.jpg
+ // 2. uploads/2024/01/image.jpg
// 3. 2024/01/image.jpg (direct uploads content)
// 4. some-folder/wp-content/uploads/2024/01/image.jpg
-
+
// Look for wp-content/uploads pattern
if (preg_match('/.*?wp-content\/uploads\/(.+)$/', $zipPath, $matches)) {
return $matches[1];
}
-
+
// Look for direct uploads pattern (excluding if it's a folder name that happens to be "uploads")
if (preg_match('/.*?\/uploads\/(.+)$/', $zipPath, $matches)) {
return $matches[1];
}
-
+
// If the ZIP contains just the contents of uploads folder
// (common when users zip the uploads folder content directly)
// Check if it looks like a date structure or has media file extensions
$pathParts = explode('/', $zipPath);
$firstPart = $pathParts[0] ?? '';
-
+
// If it starts with a year (2020-2030) or common folder names
if (preg_match('/^(20[2-3][0-9]|sites|media)/', $firstPart)) {
return $zipPath;
}
-
+
// If it's a media file in root
$extension = strtolower(pathinfo($zipPath, PATHINFO_EXTENSION));
$mediaExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'mp4', 'mp3'];
if (in_array($extension, $mediaExtensions)) {
return $zipPath;
}
-
+
// Default: use as-is but remove any leading directories that don't look like uploads
return $zipPath;
}
/**
* Get the expected local path structure for a WordPress media URL
- *
+ *
* This method shows how WordPress URLs will be mapped to local folders.
* Example: wp-content/uploads/2024/01/image.jpg -> images/imports/2024/01/image.jpg
*
@@ -1762,7 +1765,7 @@ public function getExpectedLocalPath(string $wordpressUrl): ?string
}
$uploadPath = $parsedUrl['path'];
-
+
// Check for standard WordPress structure: /wp-content/uploads/
if (strpos($uploadPath, '/wp-content/uploads/') !== false) {
$pattern = '/.*\/wp-content\/uploads\/(.+)$/';
@@ -1770,7 +1773,7 @@ public function getExpectedLocalPath(string $wordpressUrl): ?string
return 'images/' . $this->storageDir . '/' . $matches[1];
}
}
-
+
// Check for direct uploads structure: /uploads/
if (strpos($uploadPath, '/uploads/') !== false) {
$pattern = '/.*\/uploads\/(.+)$/';
diff --git a/src/component/admin/src/Model/ProcessorModel.php b/src/component/admin/src/Model/ProcessorModel.php
index 974feda..9a1a217 100644
--- a/src/component/admin/src/Model/ProcessorModel.php
+++ b/src/component/admin/src/Model/ProcessorModel.php
@@ -7,23 +7,23 @@
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
-namespace Binary\Component\CmsMigrator\Administrator\Model;
+namespace Joomla\Component\CmsMigrator\Administrator\Model;
\defined('_JEXEC') or die;
+use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
+use Joomla\CMS\Filter\OutputFilter;
+use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
+use Joomla\CMS\Table\MenuType;
use Joomla\CMS\Table\Table;
-use Joomla\CMS\User\UserHelper;
-use Joomla\CMS\Helper\TagsHelper;
-use Joomla\CMS\Filter\OutputFilter;
use Joomla\CMS\Uri\Uri;
-use Binary\Component\CmsMigrator\Administrator\Model\MediaModel;
-use Joomla\CMS\Component\ComponentHelper;
-use Joomla\Component\Menus\Administrator\Table\MenuTable;
-use Joomla\CMS\Table\MenuType;
+use Joomla\CMS\User\UserHelper;
+use Joomla\Component\CmsMigrator\Administrator\Model\MediaModel;
use Joomla\Component\Content\Administrator\Table\ArticleTable;
+use Joomla\Component\Menus\Administrator\Table\MenuTable;
/**
* Processor Model
@@ -131,7 +131,7 @@ private function processJson(array $data, string $sourceUrl = '', array $ftpConf
$userMap = $userResult['map'];
$result['errors'] = array_merge($result['errors'], $userResult['errors']);
}
-
+
$categoryMap = [];
$tagMap = [];
if (!empty($data['taxonomies'])) {
@@ -216,15 +216,15 @@ private function processWordpress(array $data, string $sourceUrl = '', array $ft
$this->executeInTransaction(function () use ($data, $sourceUrl, $ftpConfig, $importAsSuperUser, &$result) {
$mediaModel = $this->initializeMediaModel($ftpConfig);
$superUserId = $importAsSuperUser ? $this->app->getIdentity()->id : null;
-
+
// Process tags first if they exist in the data
$tagMap = [];
if (!empty($data['allTags']) && is_array($data['allTags'])) {
$tagMap = $this->processWordpressTags($data['allTags'], $result['counts']);
}
-
+
$total = count($data['itemListElement']);
-
+
// Use batch processing based on the number of articles
$this->processBatchedWordpressArticles($data['itemListElement'], $result, $mediaModel, $ftpConfig, $sourceUrl, $superUserId, $total, $tagMap);
@@ -356,12 +356,12 @@ private function processBatch(array $batch, array &$result, ?MediaModel $mediaMo
$article = $element['item'];
$content = $article['articleBody'] ?? '';
-
+
if ($mediaModel && !empty($content)) {
// Extract media URLs and prepare for batch download
$mediaUrls = $mediaModel->extractImageUrlsFromContent($content);
$updatedContent = $content;
-
+
// Update content with planned Joomla URLs (before download)
foreach ($mediaUrls as $originalUrl) {
$plannedUrl = $mediaModel->getPlannedJoomlaUrl($originalUrl);
@@ -370,10 +370,10 @@ private function processBatch(array $batch, array &$result, ?MediaModel $mediaMo
$allMediaUrls[$originalUrl] = $plannedUrl;
}
}
-
+
$article['articleBody'] = $updatedContent;
}
-
+
$batchData[] = $article;
}
@@ -467,9 +467,9 @@ private function processWordpressArticle(array $article, array &$result, ?MediaM
];
// Save the article
- $mvcFactory = Factory::getApplication()->bootComponent('com_content')
+ $contentMvcFactory = Factory::getApplication()->bootComponent('com_content')
->getMVCFactory();
- $articleModel = $mvcFactory->createModel('Article', 'Administrator', ['ignore_request' => true]);
+ $articleModel = $contentMvcFactory->createModel('Article', 'Administrator', ['ignore_request' => true]);
if (!$articleModel->save($articleData)) {
throw new \RuntimeException('Failed to save article: ' . $articleModel->getError());
}
@@ -490,7 +490,7 @@ private function processWordpressArticle(array $article, array &$result, ?MediaM
$tagIds[] = $tagMap[$tagSlug];
}
}
-
+
if (!empty($tagIds)) {
$this->linkTagsToArticle($articleId, $tagIds);
}
@@ -566,19 +566,15 @@ private function processTaxonomies(array $taxonomies, array &$counts): array
}
// Link parent categories
- try
- {
+ try {
// 1. Get the Category Model instance *once* before the loop.
$categoriesMvcFactory = Factory::getApplication()->bootComponent('com_categories')->getMVCFactory();
$categoryModel = $categoriesMvcFactory->createModel('Category', 'Administrator', ['ignore_request' => true]);
- if (!$categoryModel)
- {
+ if (!$categoryModel) {
throw new \RuntimeException('Could not create the Category model.');
}
- }
- catch (\Exception $e)
- {
+ } catch (\Exception $e) {
// If the model can't be created, it's a fatal error, so we stop.
throw new \RuntimeException('Could not create the Category model.');
$result['errors'][] = 'Fatal Error: Could not initialize the category model. ' . $e->getMessage();
@@ -586,18 +582,15 @@ private function processTaxonomies(array $taxonomies, array &$counts): array
}
// 2. Loop through your source map to find parent-child relationships.
- foreach ($categorySourceData as $sourceTermId => $term)
- {
+ foreach ($categorySourceData as $sourceTermId => $term) {
$srcParent = (int) ($term['parent'] ?? 0);
// 3. Check if this term has a parent and if that parent has been mapped to a Joomla ID.
- if ($srcParent > 0 && isset($result['map'][$sourceTermId], $result['map'][$srcParent]))
- {
+ if ($srcParent > 0 && isset($result['map'][$sourceTermId], $result['map'][$srcParent])) {
$childId = $result['map'][$sourceTermId];
$parentId = $result['map'][$srcParent];
- try
- {
+ try {
// 4. Prepare the data and save. The model handles all the complex logic
// of loading the category and recalculating the tree structure.
$dataToSave = [
@@ -605,13 +598,10 @@ private function processTaxonomies(array $taxonomies, array &$counts): array
'parent_id' => $parentId,
];
- if (!$categoryModel->save($dataToSave))
- {
+ if (!$categoryModel->save($dataToSave)) {
throw new \RuntimeException($categoryModel->getError());
}
- }
- catch (\Exception $e)
- {
+ } catch (\Exception $e) {
// If one category fails, we record the error and continue with the rest.
$result['errors'][] = sprintf(
'Failed to set parent for category ID %d (child of %d): %s',
@@ -625,7 +615,7 @@ private function processTaxonomies(array $taxonomies, array &$counts): array
return $result;
}
-
+
/**
* Processes WordPress tags from the allTags array in the JSON structure.
*
@@ -657,7 +647,7 @@ private function processWordpressTags(array $tags, array &$counts): array
return $tagMap;
}
-
+
/**
* Processes a batch of posts (articles) from the JSON import.
*
@@ -676,7 +666,7 @@ private function processPosts(array $posts, array $userMap, array $categoryMap,
{
$result = ['imported' => 0, 'skipped' => 0, 'errors' => [], 'map' => []];
$totalPosts = count($posts);
-
+
if ($totalPosts === 0) {
return $result;
}
@@ -729,9 +719,9 @@ private function processPosts(array $posts, array $userMap, array $categoryMap,
private function processJsonPostsBatch(array $batch, array $userMap, array $categoryMap, array $tagMap, ?MediaModel $mediaModel, array $ftpConfig, string $sourceUrl, int $processedCount, int $total, int $batchNumber, int $totalBatches, array &$counts): array
{
$result = ['imported' => 0, 'skipped' => 0, 'errors' => [], 'map' => []];
- $mvcFactory = Factory::getApplication()->bootComponent('com_content')
+ $contentMvcFactory = Factory::getApplication()->bootComponent('com_content')
->getMVCFactory();
- $articleModel = $mvcFactory->createModel('Article', 'Administrator', ['ignore_request' => true]);
+ $articleModel = $contentMvcFactory->createModel('Article', 'Administrator', ['ignore_request' => true]);
$defaultCatId = $this->getDefaultCategoryId();
$this->updateProgress(
@@ -745,12 +735,12 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
foreach ($batch as $postId => $post) {
$content = $post['post_content'] ?? '';
-
+
if ($mediaModel && !empty($content)) {
// Extract media URLs and prepare for batch download
$mediaUrls = $mediaModel->extractImageUrlsFromContent($content);
$updatedContent = $content;
-
+
// Update content with planned Joomla URLs (before download)
foreach ($mediaUrls as $originalUrl) {
$plannedUrl = $mediaModel->getPlannedJoomlaUrl($originalUrl);
@@ -759,13 +749,13 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
$allMediaUrls[$originalUrl] = $plannedUrl;
}
}
-
+
$post['post_content'] = $updatedContent;
} elseif (!$mediaModel && !empty($content)) {
// Convert WordPress URLs to Joomla URLs even when media migration is disabled
$post['post_content'] = $this->convertWordPressUrlsToJoomla($content, is_array($ftpConfig) ? $ftpConfig : []);
}
-
+
$batchData[$postId] = $post;
}
@@ -792,7 +782,7 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
$catId = $categoryMap[$primary['term_id']];
}
}
-
+
$articleData = [
'id' => 0,
'title' => $post['post_title'],
@@ -811,13 +801,13 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
}
$newId = $articleModel->getItem()->id;
-
+
// Map WordPress post ID to Joomla article ID
$result['map'][$postId] = $newId;
-
+
// Link tags to the article
$tagIds = [];
-
+
// Process tags from terms['post_tag'] (structured data)
if (!empty($post['terms']['post_tag']) && !empty($tagMap)) {
foreach ($post['terms']['post_tag'] as $tag) {
@@ -826,7 +816,7 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
}
}
}
-
+
// Process tags from tags_input (simple array) - create tags if they don't exist
if (!empty($post['tags_input']) && is_array($post['tags_input'])) {
foreach ($post['tags_input'] as $tagName) {
@@ -844,12 +834,12 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate
}
}
}
-
+
// Link all collected tags to the article
if (!empty($tagIds)) {
$this->linkTagsToArticle($newId, array_unique($tagIds));
}
-
+
if (!empty($post['metadata']) && is_array($post['metadata'])) {
$fields = [];
foreach ($post['metadata'] as $key => $vals) {
@@ -881,7 +871,7 @@ protected function initializeMediaModel(array $ftpConfig): ?MediaModel
{
// Check if media migration is enabled
$connectionType = $ftpConfig['connection_type'] ?? '';
-
+
// For ZIP uploads, we don't need host credentials, just the connection type
if ($connectionType === 'zip') {
$mediaModel = $this->getMVCFactory()->createModel('Media', 'Administrator', ['ignore_request' => true]);
@@ -889,16 +879,16 @@ protected function initializeMediaModel(array $ftpConfig): ?MediaModel
? $ftpConfig['media_custom_dir']
: 'imports';
$mediaModel->setStorageDirectory($storageDir);
-
+
// Process ZIP upload immediately when initializing the model
if (!$mediaModel->connect($ftpConfig)) {
$this->app->enqueueMessage('Failed to process ZIP upload for media migration', 'error');
return null;
}
-
+
return $mediaModel;
}
-
+
// For FTP/FTPS/SFTP, require host configuration
if (empty($ftpConfig['host'])) {
return null;
@@ -941,25 +931,25 @@ protected function convertWordPressUrlsToJoomla(string $content, array $ftpConfi
// Pattern to match WordPress media URLs
// Matches: http://example.com/wp-content/uploads/2024/01/image.jpg
$pattern = '/https?:\/\/[^\/]+\/wp-content\/uploads\/([^\s"\'<>]+\.(jpg|jpeg|png|gif|webp|pdf|doc|docx|mp4|mp3|zip))/i';
- $updatedContent = preg_replace_callback($pattern, function($matches) use ($joomlaBaseUrl, $storageDir) {
+ $updatedContent = preg_replace_callback($pattern, function ($matches) use ($joomlaBaseUrl, $storageDir) {
$wpPath = $matches[1]; // e.g., "2024/01/image.jpg"
-
+
// Convert to Joomla URL maintaining the WordPress folder structure
$joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $wpPath;
-
+
return $joomlaUrl;
}, $content);
// Also handle relative WordPress URLs that might not have the full domain
// Pattern: /wp-content/uploads/2024/01/image.jpg
$relativePattern = '/\/wp-content\/uploads\/([^\s"\'<>]+\.(jpg|jpeg|png|gif|webp|pdf|doc|docx|mp4|mp3|zip))/i';
-
- $updatedContent = preg_replace_callback($relativePattern, function($matches) use ($joomlaBaseUrl, $storageDir) {
+
+ $updatedContent = preg_replace_callback($relativePattern, function ($matches) use ($joomlaBaseUrl, $storageDir) {
$wpPath = $matches[1]; // e.g., "2024/01/image.jpg"
-
+
// Convert to Joomla URL maintaining the WordPress folder structure
$joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $wpPath;
-
+
return $joomlaUrl;
}, $updatedContent);
@@ -1046,7 +1036,7 @@ protected function getOrCreateTag(string $tagName, array &$counts, ?array $sourc
if (!$tagId) {
$tagsMvcFactory = Factory::getApplication()->bootComponent('com_tags')->getMVCFactory();
$tagModel = $tagsMvcFactory->createModel('Tag', 'Administrator', ['ignore_request' => true]);
-
+
$tagData = [
'id' => 0,
'title' => $tagName,
@@ -1061,7 +1051,7 @@ protected function getOrCreateTag(string $tagName, array &$counts, ?array $sourc
// The model's save method will handle nested set logic automatically
throw new \RuntimeException('Failed to save tag: ' . $tagModel->getError());
}
-
+
$counts['taxonomies']++;
$tagId = $tagModel->getItem()->id;
}
@@ -1089,7 +1079,7 @@ protected function getOrCreateUser(string $username, string $email, array &$coun
if ($dummyHash === null) {
$randomPassword = bin2hex(random_bytes(8));
$dummyHash = UserHelper::hashPassword($randomPassword);
-
+
// Save the password to admin only to reuse or send mass mails
// file_put_contents(JPATH_ROOT . '/migration_password.txt', "Temp password: $randomPassword\n");
}
@@ -1099,7 +1089,7 @@ protected function getOrCreateUser(string $username, string $email, array &$coun
'name' => $sourceData['display_name'] ?? $username,
'username' => $username,
'email' => $email,
- 'password' => $dummyHash,
+ 'password' => $sourceData['user_pass'] ?? $dummyHash,
'registerDate' => isset($sourceData['user_registered']) ? (new Date($sourceData['user_registered']))->toSql() : Factory::getDate()->toSql(),
'groups' => [2], // Registered
'requireReset' => 1,
@@ -1165,14 +1155,13 @@ protected function getOrCreateCustomField(string $fieldName): int
->where('name = ' . $db->quote($fieldName));
$existingId = (int) $db->setQuery($query)->loadResult();
- if ($existingId)
- {
+ if ($existingId) {
return $existingId;
}
$fieldsMvcFactory = Factory::getApplication()->bootComponent('com_fields')->getMVCFactory();
$fieldModel = $fieldsMvcFactory->createModel('Field', 'Administrator', ['ignore_request' => true]);
-
+
$fieldData = [
'id' => 0,
'title' => ucwords(str_replace(['_', '-'], ' ', $fieldName)),
@@ -1187,13 +1176,10 @@ protected function getOrCreateCustomField(string $fieldName): int
'params' => '',
];
- try
- {
- if (! $fieldModel->save($fieldData))
- {
+ try {
+ if (! $fieldModel->save($fieldData)) {
$err = $fieldModel->getError();
- if (strpos($err, 'COM_FIELDS_ERROR_UNIQUE_NAME') !== false)
- {
+ if (strpos($err, 'COM_FIELDS_ERROR_UNIQUE_NAME') !== false) {
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__fields'))
@@ -1206,9 +1192,7 @@ protected function getOrCreateCustomField(string $fieldName): int
}
return (int) $fieldModel->getItem()->id;
- }
- catch (\Exception $e)
- {
+ } catch (\Exception $e) {
$this->app->enqueueMessage(
sprintf('Error creating custom field "%s": %s', $fieldName, $e->getMessage()),
'warning'
@@ -1249,7 +1233,7 @@ private function getDefaultCategoryId(): int
->from($db->quoteName('#__categories'))
->where($db->quoteName('path') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
-
+
return (int) $db->setQuery($query)->loadResult() ?: 2; // Fallback to root
}
@@ -1295,8 +1279,8 @@ protected function formatDate(?string $dateString): string
protected function linkTagsToArticle(int $articleId, array $tagIds): void
{
try {
- $mvcFactory = Factory::getApplication()->bootComponent('com_content')->getMVCFactory();
- $articleTable = $mvcFactory->createTable('Article', 'Administrator');
+ $contentMvcFactory = Factory::getApplication()->bootComponent('com_content')->getMVCFactory();
+ $articleTable = $contentMvcFactory->createTable('Article', 'Administrator');
if (!$articleTable->load($articleId)) {
throw new \RuntimeException("Article with ID {$articleId} not found.");
@@ -1346,7 +1330,7 @@ protected function getUniqueAlias(string $alias): string
}
return $alias;
}
-
+
/**
* Checks if a given alias exists in the content table.
*
@@ -1403,7 +1387,7 @@ protected function processMenus(array $menus, array $contentMap, array &$counts)
// Step 1: Create the Joomla Menu container (Menu Type) if it doesn't exist.
$menusMvcFactory = Factory::getApplication()->bootComponent('com_menus')->getMVCFactory();
$menuTypeTable = $menusMvcFactory->createTable('MenuType', 'Administrator');
-
+
// Check if the menu type already exists to avoid errors on re-run
if (!$menuTypeTable->load(['menutype' => $wpMenuName])) {
$menuTypeData = [
@@ -1479,7 +1463,7 @@ protected function processMenus(array $menus, array $contentMap, array &$counts)
$menuItemTable = $menusMvcFactory->createTable('Menu', 'Administrator');
list($link, $type) = $this->generateJoomlaLink($item, $contentMap);
-
+
$alias = OutputFilter::stringURLSafe($item['title']);
$menuItemData = [
@@ -1507,7 +1491,6 @@ protected function processMenus(array $menus, array $contentMap, array &$counts)
$wpToJoomlaMenuItemMap[$item['ID']] = $menuItemTable->id;
$counts['menu_items']++;
}
-
} catch (\Exception $e) {
$result['errors'][] = sprintf('CRITICAL ERROR importing menu "%s": %s', $wpMenuName, $e->getMessage());
}
@@ -1533,7 +1516,7 @@ protected function generateJoomlaLink(array $wpItem, array $contentMap): array
// Use the content map to find the new Joomla Article ID
$wpId = $wpItem['object_id'] ?? 0;
$joomlaId = $contentMap['posts'][$wpId] ?? 0;
-
+
if ($joomlaId) {
return ['index.php?option=com_content&view=article&id=' . (int) $joomlaId, 'component'];
}
@@ -1548,13 +1531,13 @@ protected function generateJoomlaLink(array $wpItem, array $contentMap): array
return ['index.php?option=com_content&view=category&layout=blog&id=' . (int) $joomlaId, 'component'];
}
break;
-
+
case 'custom':
default:
// For custom links, just use the URL directly.
return [$wpItem['url'] ?? '#', 'url'];
}
-
+
// Fallback if the mapped content was not found
return ['#', 'url'];
}
@@ -1585,8 +1568,8 @@ protected function getComponentIdFromLink(string $link): int
return $component->id;
}
}
-
+
// Default to com_wrapper if no specific component is found
return ComponentHelper::getComponent('com_wrapper')->id;
}
-}
\ No newline at end of file
+}
diff --git a/src/component/admin/src/Table/ArticleTable.php b/src/component/admin/src/Table/ArticleTable.php
index bb56e43..d468a23 100644
--- a/src/component/admin/src/Table/ArticleTable.php
+++ b/src/component/admin/src/Table/ArticleTable.php
@@ -7,7 +7,7 @@
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
-namespace Binary\Component\CmsMigrator\Administrator\Table;
+namespace Joomla\Component\CmsMigrator\Administrator\Table;
\defined('_JEXEC') or die;
@@ -88,4 +88,4 @@ public function check()
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/component/admin/src/View/Cpanel/HtmlView.php b/src/component/admin/src/View/Cpanel/HtmlView.php
index f969eea..7ff42c3 100644
--- a/src/component/admin/src/View/Cpanel/HtmlView.php
+++ b/src/component/admin/src/View/Cpanel/HtmlView.php
@@ -7,7 +7,7 @@
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
-namespace Binary\Component\CmsMigrator\Administrator\View\Cpanel;
+namespace Joomla\Component\CmsMigrator\Administrator\View\Cpanel;
\defined('_JEXEC') or die;
@@ -116,7 +116,7 @@ protected function addScripts(): void
'ftpFieldsRequired' => Text::_('COM_CMSMIGRATOR_MEDIA_FTP_FIELDS_REQUIRED'),
'zipFileError' => Text::_('COM_CMSMIGRATOR_MEDIA_ZIP_FILE_ERROR')
]);
-
+
// Load language strings for JavaScript
Text::script('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_TESTING');
Text::script('COM_CMSMIGRATOR_MEDIA_TEST_CONNECTION_SUCCESS');
diff --git a/src/component/admin/tmpl/cpanel/default.php b/src/component/admin/tmpl/cpanel/default.php
index 49585db..f6c2edc 100644
--- a/src/component/admin/tmpl/cpanel/default.php
+++ b/src/component/admin/tmpl/cpanel/default.php
@@ -130,7 +130,7 @@
$selected = $this->form->getValue('media_storage_mode', null, 'root');
foreach ($mediaStorageOptions as $option) :
$isActive = ($selected == $option->value) ? 'active' : '';
- ?>
+ ?>
value) ? 'checked' : ''; ?>>