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 + +![Installation Process](../assets/Module_installation.png) + +βœ… **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: + +![Module Configuration](../assets/Module_configuration.png) + +| 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) +![Guest User View](../assets/Module-guest-view.png) + +Shows: +- Migration explanation +- Password reset requirement +- Login button + +### Logged-In Users +![Logged User View](../assets/Module-logged-view.png) + +Shows: +- Password reset notice +- Direct "Reset Password" button + +## πŸ”§ Management + +### Editing the Module +**Extensions** β†’ **Modules** β†’ **Find your module** β†’ **Click title to edit** + +![Module Management](../assets/Module_management.png) + +### 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.

+ +
+ + + + + + +
Export Type + +
+ +
+
+ $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' : ''; ?>>