diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9f797f..4204f81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,10 +68,10 @@ jobs: run: | for i in {1..40}; do if curl -f http://localhost:8080/installation/index.php > /dev/null 2>&1; then - echo "✅ Joomla installer is ready" + echo "Joomla installer is ready" break fi - echo "⏳ Waiting for Joomla installer..." + echo "Waiting for Joomla installer..." sleep 10 done @@ -122,29 +122,74 @@ jobs: id: version run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - name: Create component zip with version + - name: Extract wordpress plugin version + run: | + version_wordpress=$(grep -oP '\K[^<]+' src/plugins/migration/wordpress/wordpress.xml) + echo "version_wordpress=$version_wordpress" >> $GITHUB_ENV + + - name: Extract wp-all-export plugin version + run: | + version_wp_all_export=$(grep -oP '\* Version:\s*\K[^\s]+' src/cms-export-plugins/ja-wp-all-export/wp-all-export.php) + echo "version_wp_all_export=$version_wp_all_export" >> $GITHUB_ENV + + - name: Update XML versions + run: | + sed -i "s|.*|${{ steps.version.outputs.version }}|" src/component/cmsmigrator.xml + sed -i "s|.*|${{ steps.version.outputs.version }}|" src/modules/mod_migrationnotice/mod_migrationnotice.xml + sed -i "s|.*|${{ steps.version.outputs.version }}|" pkg_cmsmigrator.xml + + - name: Create component zip run: | mkdir -p cypress/fixtures cd src/component zip -r ../../cypress/fixtures/com_cmsmigrator_v${{ steps.version.outputs.version }}.zip . - - name: Verify zip file exists - run: ls -lh cypress/fixtures/ + - name: Create module zip + run: | + cd src/modules/mod_migrationnotice + zip -r ../../../cypress/fixtures/mod_migrationnotice.zip . - - name: Update XML version + - name: Create package zip run: | - sed -i "s|.*|${{ steps.version.outputs.version }}|" src/component/cmsmigrator.xml + # Copy package manifest and language files to fixtures + cp pkg_cmsmigrator.xml cypress/fixtures/ + cp -r language cypress/fixtures/ + + # Create constituents directory and move component/module zips there + cd cypress/fixtures + mkdir -p constituents + cp com_cmsmigrator_v${{ steps.version.outputs.version }}.zip constituents/com_cmsmigrator.zip + cp mod_migrationnotice.zip constituents/ + + # Create the package zip with the new structure + zip -r pkg_cmsmigrator_v${{ steps.version.outputs.version }}.zip \ + pkg_cmsmigrator.xml \ + constituents/ \ + language/ + + - name: Create wordpress plugin zip + run: | + cd src/plugins/migration/wordpress + zip -r ../../../../cypress/fixtures/plg_wordpress_v${{ env.version_wordpress }}.zip . + + - name: Create wp-all-export plugin zip + run: | + cd src/cms-export-plugins/ja-wp-all-export + zip -r ../../../cypress/fixtures/ja_wp_all_export_v${{ env.version_wp_all_export }}.zip . + + - name: Verify zip files exist + run: ls -lh cypress/fixtures/ - - name: Commit version update + - name: Commit version updates run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add src/component/cmsmigrator.xml + git add src/component/cmsmigrator.xml src/modules/mod_migrationnotice/mod_migrationnotice.xml pkg_cmsmigrator.xml git commit -m "Bump version to ${{ steps.version.outputs.version }}" || echo "No changes to commit" git push origin main - - name: Upload asset to GitHub Release + - name: Upload assets to GitHub Release run: | - gh release upload ${{ github.event.release.tag_name }} cypress/fixtures/com_cmsmigrator_v${{ steps.version.outputs.version }}.zip --clobber + gh release upload ${{ github.event.release.tag_name }} cypress/fixtures/pkg_cmsmigrator_v${{ steps.version.outputs.version }}.zip cypress/fixtures/plg_wordpress_v${{ env.version_wordpress }}.zip cypress/fixtures/ja_wp_all_export_v${{ env.version_wp_all_export }}.zip --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/language/en-GB/pkg_cmsmigrator.ini b/language/en-GB/pkg_cmsmigrator.ini new file mode 100644 index 0000000..9b0ae7e --- /dev/null +++ b/language/en-GB/pkg_cmsmigrator.ini @@ -0,0 +1,6 @@ +; Package Language File +PKG_CMSMIGRATOR="CMS Migrator Package" +PKG_CMSMIGRATOR_DESCRIPTION="This component helps in migrating websites from other platforms to Joomla." +PKG_CMSMIGRATOR_INSTALL_SUCCESS="CMS Migrator Package has been successfully installed!" +PKG_CMSMIGRATOR_UNINSTALL_SUCCESS="CMS Migrator Package has been successfully uninstalled!" +PKG_CMSMIGRATOR_UPDATE_SUCCESS="CMS Migrator Package has been successfully updated!" \ No newline at end of file diff --git a/language/en-GB/pkg_cmsmigrator.sys.ini b/language/en-GB/pkg_cmsmigrator.sys.ini new file mode 100644 index 0000000..574c08b --- /dev/null +++ b/language/en-GB/pkg_cmsmigrator.sys.ini @@ -0,0 +1,3 @@ +; Package System Language File +PKG_CMSMIGRATOR="CMS Migrator Package" +PKG_CMSMIGRATOR_DESCRIPTION="This component helps in migrating websites from other platforms to Joomla." \ No newline at end of file diff --git a/pkg_cmsmigrator.xml b/pkg_cmsmigrator.xml new file mode 100644 index 0000000..8656fd1 --- /dev/null +++ b/pkg_cmsmigrator.xml @@ -0,0 +1,39 @@ + + + + PKG_CMSMIGRATOR + cmsmigrator + + Joomla Academy + + 2025-10-03 + (C) 2025 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + 0.9.0 + PKG_CMSMIGRATOR_DESCRIPTION + Joomla Academy + https://github.com/joomla-projects/JA-Advanced-Migration-Tool + + + + com_cmsmigrator.zip + mod_migrationnotice.zip + + + + + en-GB/pkg_cmsmigrator.ini + en-GB/pkg_cmsmigrator.sys.ini + + + + + + \ No newline at end of file diff --git a/src/cms-export-plugins/ja-wp-all-export/wp-all-export.php b/src/cms-export-plugins/ja-wp-all-export/wp-all-export.php new file mode 100644 index 0000000..e1204d4 --- /dev/null +++ b/src/cms-export-plugins/ja-wp-all-export/wp-all-export.php @@ -0,0 +1,941 @@ + +
+

+

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/cms-export-plugins/wp-all-export/wp-all-export.php b/src/cms-export-plugins/wp-all-export/wp-all-export.php index 5600370..88dc7ad 100644 --- a/src/cms-export-plugins/wp-all-export/wp-all-export.php +++ b/src/cms-export-plugins/wp-all-export/wp-all-export.php @@ -3,7 +3,7 @@ * Plugin Name: All Export * Description: Export all WordPress core data like Users, Posts, Pages, Categories, and Taxonomies. * Version: 1.0.0 - * Author: Rahul Singh + * Author: Joomla Academy * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: wp-all-export diff --git a/src/component/admin/src/Model/MediaModel.php b/src/component/admin/src/Model/MediaModel.php index 09cad58..9ff5ae2 100644 --- a/src/component/admin/src/Model/MediaModel.php +++ b/src/component/admin/src/Model/MediaModel.php @@ -133,7 +133,7 @@ public function __construct($config = []) /** * Sets the storage directory. * - * The storage directory will contain the WordPress media files organized + * The storage directory will contain the Source 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.). @@ -276,9 +276,9 @@ protected function extractImageUrls(string $content): array } // Match standard WordPress uploads URLs - preg_match_all('/https?:\/\/[^\/]+\/wp-content\/uploads\/[^\s"\'<>]+\.(jpg|jpeg|png|gif|webp)/i', $content, $wpMatches); - if (!empty($wpMatches[0])) { - $imageUrls = array_merge($imageUrls, $wpMatches[0]); + preg_match_all('/https?:\/\/[^\/]+\/wp-content\/uploads\/[^\s"\'<>]+\.(jpg|jpeg|png|gif|webp)/i', $content, $urlMatches); + if (!empty($urlMatches[0])) { + $imageUrls = array_merge($imageUrls, $urlMatches[0]); } // Match direct uploads folder URLs @@ -310,22 +310,22 @@ protected function downloadAndProcessImage(string $imageUrl): ?string $uploadPath = $parsedUrl['path']; // Check for different WordPress upload path patterns - $isWordPressUpload = false; + $isSourceUpload = false; $relativePath = ''; if (strpos($uploadPath, '/wp-content/uploads/') !== false) { // Standard WordPress structure: /wp-content/uploads/... - $isWordPressUpload = true; + $isSourceUpload = true; preg_match('/.*\/wp-content\/uploads\/(.+)$/', $uploadPath, $matches); $relativePath = $matches[1] ?? ''; } elseif (strpos($uploadPath, '/uploads/') !== false) { // Direct uploads folder: /uploads/... - $isWordPressUpload = true; + $isSourceUpload = true; preg_match('/.*\/uploads\/(.+)$/', $uploadPath, $matches); $relativePath = $matches[1] ?? ''; } - if (!$isWordPressUpload || empty($relativePath)) { + if (!$isSourceUpload || empty($relativePath)) { $this->app->enqueueMessage("Not a WordPress upload path: $uploadPath", 'warning'); return null; } @@ -609,7 +609,7 @@ protected function autoDetectDocumentRoot(): void foreach ($testScenarios as [$root, $contentPath]) { $canAccess = false; - $hasWordPressContent = false; + $hasSourceContent = false; if ($this->connectionType === 'sftp' && $this->sftpConnection) { try { @@ -621,13 +621,13 @@ protected function autoDetectDocumentRoot(): void // Check for WordPress content structure if ($this->sftpConnection->is_dir($checkPath)) { - $hasWordPressContent = true; + $hasSourceContent = true; // For wp-content, also check for uploads subdirectory if ($contentPath === 'wp-content') { $uploadsPath = $checkPath . '/uploads'; if ($this->sftpConnection->is_dir($uploadsPath)) { - $hasWordPressContent = true; + $hasSourceContent = true; } } } @@ -644,12 +644,12 @@ protected function autoDetectDocumentRoot(): void // Check for WordPress content structure if (@ftp_chdir($this->ftpConnection, $contentPath)) { - $hasWordPressContent = true; + $hasSourceContent = true; // For wp-content, also check for uploads subdirectory if ($contentPath === 'wp-content') { if (@ftp_chdir($this->ftpConnection, 'uploads')) { - $hasWordPressContent = true; + $hasSourceContent = true; // Return to wp-content @ftp_chdir($this->ftpConnection, '..'); } @@ -667,7 +667,7 @@ protected function autoDetectDocumentRoot(): void } } - if ($canAccess && $hasWordPressContent) { + if ($canAccess && $hasSourceContent) { $this->documentRoot = $root ?: '.'; $this->documentRootDetected = true; @@ -924,7 +924,7 @@ protected function processZipUpload(array $config): bool // Extract individual file to handle path structure better $fileData = $zip->getFromIndex($i); if ($fileData !== false) { - // Handle WordPress uploads folder structure + // Handle Source uploads folder structure $relativePath = $this->normalizeUploadPath($filename); $targetPath = $extractPath . $relativePath; @@ -976,7 +976,7 @@ protected function processContentForZipUpload(string $content, array $imageUrls) foreach ($imageUrls as $originalUrl) { try { - // Extract the file path from the WordPress URL + // Extract the file path from the Source URL // Typical WordPress URL: http://example.com/wp-content/uploads/2024/01/image.jpg $newUrl = $this->findExtractedImageUrl($originalUrl); if ($newUrl) { @@ -1124,9 +1124,9 @@ protected function disconnectSftp(): void * * @since 1.0.0 */ - public function getPlannedJoomlaUrl(string $wordpressUrl): ?string + public function getPlannedJoomlaUrl(string $sourceUrl): ?string { - $parsedUrl = parse_url($wordpressUrl); + $parsedUrl = parse_url($sourceUrl); if (!$parsedUrl || empty($parsedUrl['path'])) { return null; } @@ -1287,22 +1287,22 @@ protected function prepareDownloadPaths(string $imageUrl): array $uploadPath = $parsedUrl['path']; // Check for different WordPress upload path patterns - $isWordPressUpload = false; + $isSourceUpload = false; $relativePath = ''; if (strpos($uploadPath, '/wp-content/uploads/') !== false) { // Standard WordPress structure: /wp-content/uploads/... - $isWordPressUpload = true; + $isSourceUpload = true; preg_match('/.*\/wp-content\/uploads\/(.+)$/', $uploadPath, $matches); $relativePath = $matches[1] ?? ''; } elseif (strpos($uploadPath, '/uploads/') !== false) { // Direct uploads folder: /uploads/... - $isWordPressUpload = true; + $isSourceUpload = true; preg_match('/.*\/uploads\/(.+)$/', $uploadPath, $matches); $relativePath = $matches[1] ?? ''; } - if (!$isWordPressUpload || empty($relativePath)) { + if (!$isSourceUpload || empty($relativePath)) { return []; } @@ -1757,9 +1757,9 @@ protected function normalizeUploadPath(string $zipPath): string * * @since 1.0.0 */ - public function getExpectedLocalPath(string $wordpressUrl): ?string + public function getExpectedLocalPath(string $sourceUrl): ?string { - $parsedUrl = parse_url($wordpressUrl); + $parsedUrl = parse_url($sourceUrl); if (!$parsedUrl || empty($parsedUrl['path'])) { return null; } diff --git a/src/component/admin/src/Model/ProcessorModel.php b/src/component/admin/src/Model/ProcessorModel.php index 9a1a217..4a2c3d6 100644 --- a/src/component/admin/src/Model/ProcessorModel.php +++ b/src/component/admin/src/Model/ProcessorModel.php @@ -71,7 +71,7 @@ public function process(array $data, string $sourceUrl = '', array $ftpConfig = } if (isset($data['itemListElement'])) { - return $this->processWordpress($data, $sourceUrl, $ftpConfig, $importAsSuperUser); + return $this->processSource($data, $sourceUrl, $ftpConfig, $importAsSuperUser); } throw new \RuntimeException('Invalid data format'); @@ -190,7 +190,7 @@ private function processJson(array $data, string $sourceUrl = '', array $ftpConf } /** - * Processes migration data from a WordPress JSON-LD structure. + * Processes migration data from a parsed Source JSON-LD structure. * * @param array $data The migration data. * @param string $sourceUrl The source URL. @@ -199,7 +199,7 @@ private function processJson(array $data, string $sourceUrl = '', array $ftpConf * * @return array The result of the processing. */ - private function processWordpress(array $data, string $sourceUrl = '', array $ftpConfig = [], bool $importAsSuperUser = false): array + private function processSource(array $data, string $sourceUrl = '', array $ftpConfig = [], bool $importAsSuperUser = false): array { $result = [ 'success' => true, @@ -209,7 +209,7 @@ private function processWordpress(array $data, string $sourceUrl = '', array $ft if (!isset($data['itemListElement']) || !is_array($data['itemListElement'])) { $result['success'] = false; - $result['errors'][] = 'Invalid WordPress JSON format'; + $result['errors'][] = Text::_('COM_CMSMIGRATOR_INVALID_JSON_FORMAT_FROM_PLUGIN'); return $result; } @@ -220,13 +220,13 @@ private function processWordpress(array $data, string $sourceUrl = '', array $ft // 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']); + $tagMap = $this->processSourceTags($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); + $this->processBatchedSourceArticles($data['itemListElement'], $result, $mediaModel, $ftpConfig, $sourceUrl, $superUserId, $total, $tagMap); if ($mediaModel) { $result['counts']['media'] = $mediaModel->getMediaStats()['downloaded']; @@ -252,7 +252,7 @@ private function processWordpress(array $data, string $sourceUrl = '', array $ft } /** - * Process WordPress articles in batches with parallel media downloading + * Process Source articles in batches with parallel media downloading * * @param array $articles Array of article elements * @param array &$result Result array passed by reference @@ -267,7 +267,7 @@ private function processWordpress(array $data, string $sourceUrl = '', array $ft * * @since 1.0.0 */ - private function processBatchedWordpressArticles(array $articles, array &$result, ?MediaModel $mediaModel, array $ftpConfig, string $sourceUrl, ?int $superUserId, int $total, array $tagMap = []): void + private function processBatchedSourceArticles(array $articles, array &$result, ?MediaModel $mediaModel, array $ftpConfig, string $sourceUrl, ?int $superUserId, int $total, array $tagMap = []): void { $batchSize = $this->calculateBatchSize($total); $batches = array_chunk($articles, $batchSize); @@ -385,7 +385,7 @@ private function processBatch(array $batch, array &$result, ?MediaModel $mediaMo // Step 3: Process articles sequentially foreach ($batchData as $index => $article) { try { - $this->processWordpressArticle($article, $result, null, $ftpConfig, $sourceUrl, $superUserId, $tagMap); + $this->processSourceArticle($article, $result, null, $ftpConfig, $sourceUrl, $superUserId, $tagMap); $currentProgress = (int)((($processedCount + $index + 1) / $total) * 90); $this->updateProgress( $currentProgress, @@ -407,7 +407,7 @@ private function processBatch(array $batch, array &$result, ?MediaModel $mediaMo } /** - * Processes a single WordPress article item. + * Processes a single Source article item. * * @param array $article The article data array. * @param array &$result The result array, passed by reference. @@ -420,7 +420,7 @@ private function processBatch(array $batch, array &$result, ?MediaModel $mediaMo * @return void * @throws \Exception */ - private function processWordpressArticle(array $article, array &$result, ?MediaModel $mediaModel, array $ftpConfig, string $sourceUrl, ?int $superUserId, array $tagMap = []): void + private function processSourceArticle(array $article, array &$result, ?MediaModel $mediaModel, array $ftpConfig, string $sourceUrl, ?int $superUserId, array $tagMap = []): void { if ($this->articleExists($article['headline'])) { $result['counts']['skipped']++; @@ -428,12 +428,12 @@ private function processWordpressArticle(array $article, array &$result, ?MediaM } // Clean and process content - $content = $this->cleanWordPressContent($article['articleBody'] ?? ''); + $content = $this->cleanSourceContent($article['articleBody'] ?? ''); if ($mediaModel) { $content = $mediaModel->migrateMediaInContent($ftpConfig, $content, $sourceUrl); } else { - // Convert WordPress URLs to Joomla URLs even when media migration is disabled - $content = $this->convertWordPressUrlsToJoomla($content, is_array($ftpConfig) ? $ftpConfig : []); + // Convert Source URLs to Joomla URLs even when media migration is disabled + $content = $this->convertSourceUrlsToJoomla($content, is_array($ftpConfig) ? $ftpConfig : []); } [$introtext, $fulltext] = (strpos($content, '') !== false) @@ -617,14 +617,14 @@ private function processTaxonomies(array $taxonomies, array &$counts): array } /** - * Processes WordPress tags from the allTags array in the JSON structure. + * Processes Source tags from the allTags array in the JSON structure. * * @param array $tags Array of tag data with name, slug, and description. * @param array &$counts The main counts array. * * @return array Map of tag slugs to tag IDs. */ - private function processWordpressTags(array $tags, array &$counts): array + private function processSourceTags(array $tags, array &$counts): array { $tagMap = []; @@ -752,8 +752,8 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate $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 : []); + // Convert Source URLs to Joomla URLs even when media migration is disabled + $post['post_content'] = $this->convertSourceUrlsToJoomla($content, is_array($ftpConfig) ? $ftpConfig : []); } $batchData[$postId] = $post; @@ -802,7 +802,7 @@ private function processJsonPostsBatch(array $batch, array $userMap, array $cate $newId = $articleModel->getItem()->id; - // Map WordPress post ID to Joomla article ID + // Map Source post ID to Joomla article ID $result['map'][$postId] = $newId; // Link tags to the article @@ -904,17 +904,17 @@ protected function initializeMediaModel(array $ftpConfig): ?MediaModel } /** - * Converts WordPress media URLs to Joomla-compatible URLs - * This allows users to manually copy media folders from WordPress to Joomla + * Converts Source media URLs to Joomla-compatible URLs + * This allows users to manually copy media folders from Source to Joomla * - * @param string $content The content containing WordPress URLs + * @param string $content The content containing Source URLs * @param array $ftpConfig FTP configuration to determine storage directory * * @return string The content with converted URLs * * @since 1.0.0 */ - protected function convertWordPressUrlsToJoomla(string $content, array $ftpConfig = []): string + protected function convertSourceUrlsToJoomla(string $content, array $ftpConfig = []): string { if (empty($content)) { return $content; @@ -928,27 +928,27 @@ protected function convertWordPressUrlsToJoomla(string $content, array $ftpConfi // Get the Joomla site URL $joomlaBaseUrl = Uri::root(); - // Pattern to match WordPress media URLs + // Pattern to match Source 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) { - $wpPath = $matches[1]; // e.g., "2024/01/image.jpg" + $sourcePath = $matches[1]; // e.g., "2024/01/image.jpg" - // Convert to Joomla URL maintaining the WordPress folder structure - $joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $wpPath; + // Convert to Joomla URL maintaining the Source folder structure + $joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $sourcePath; return $joomlaUrl; }, $content); - // Also handle relative WordPress URLs that might not have the full domain + // Also handle relative Source 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) { - $wpPath = $matches[1]; // e.g., "2024/01/image.jpg" + $sourcePath = $matches[1]; // e.g., "2024/01/image.jpg" - // Convert to Joomla URL maintaining the WordPress folder structure - $joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $wpPath; + // Convert to Joomla URL maintaining the Source folder structure + $joomlaUrl = $joomlaBaseUrl . 'images/' . $storageDir . '/' . $sourcePath; return $joomlaUrl; }, $updatedContent); @@ -1044,6 +1044,7 @@ protected function getOrCreateTag(string $tagName, array &$counts, ?array $sourc 'description' => $sourceData['description'] ?? '', 'published' => 1, 'access' => 1, + 'parent_id' => 1, 'language' => '*', ]; @@ -1238,13 +1239,13 @@ private function getDefaultCategoryId(): int } /** - * Cleans WordPress-specific block editor comments from content. + * Cleans Source-specific block editor comments from content. * * @param string $content The raw HTML content. * * @return string The cleaned HTML content. */ - protected function cleanWordPressContent(string $content): string + protected function cleanSourceContent(string $content): string { $content = preg_replace('//s', '', $content); $content = preg_replace('//s', '', $content); @@ -1368,10 +1369,10 @@ protected function articleExists(string $title): bool } /** - * Processes an array of WordPress menus and imports them into Joomla. + * Processes an array of Source menus and imports them into Joomla. * - * @param array $menus The array of menu data from the WordPress JSON export. - * @param array $contentMap A mapping of old WordPress IDs to new Joomla IDs. + * @param array $menus The array of menu data from the Source JSON export. + * @param array $contentMap A mapping of old Source IDs to new Joomla IDs. * Example: ['posts' => [123 => 45], 'categories' => [10 => 8]] * @param array &$counts An array to keep track of import counts. * @@ -1380,19 +1381,19 @@ protected function articleExists(string $title): bool protected function processMenus(array $menus, array $contentMap, array &$counts): array { $result = ['map' => [], 'errors' => []]; - $wpToJoomlaMenuItemMap = []; // CRITICAL: Maps old WP menu item IDs to new Joomla menu item IDs. + $sourceToJoomlaMenuItemMap = []; // CRITICAL: Maps old Source menu item IDs to new Joomla menu item IDs. - foreach ($menus as $wpMenuName => $wpMenuItems) { + foreach ($menus as $sourceMenuName => $sourceMenuItems) { try { // 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])) { + if (!$menuTypeTable->load(['menutype' => $sourceMenuName])) { $menuTypeData = [ - 'menutype' => $wpMenuName, - 'title' => ucfirst($wpMenuName), + 'menutype' => $sourceMenuName, + 'title' => ucfirst($sourceMenuName), 'description' => 'Imported from WordPress on ' . date('Y-m-d'), ]; @@ -1403,11 +1404,11 @@ protected function processMenus(array $menus, array $contentMap, array &$counts) } // Step 2: First Pass - Import only TOP-LEVEL menu items. - if (empty($wpMenuItems) || !is_array($wpMenuItems)) { + if (empty($sourceMenuItems) || !is_array($sourceMenuItems)) { continue; // Skip if there are no items } - foreach ($wpMenuItems as $item) { + foreach ($sourceMenuItems as $item) { if ((string) ($item['menu_item_parent'] ?? '0') !== '0') { continue; } @@ -1416,7 +1417,7 @@ protected function processMenus(array $menus, array $contentMap, array &$counts) list($link, $type) = $this->generateJoomlaLink($item, $contentMap); $menuItemData = [ - 'menutype' => $wpMenuName, + 'menutype' => $sourceMenuName, 'title' => $item['title'] ?? 'Untitled', 'alias' => OutputFilter::stringURLSafe($item['title']), 'path' => OutputFilter::stringURLSafe($item['title']), @@ -1436,26 +1437,26 @@ protected function processMenus(array $menus, array $contentMap, array &$counts) throw new \RuntimeException('Menu Item Save Failed (Pass 1): ' . $menuItemTable->getError()); } - // Map the old WordPress ID to the new Joomla ID for the second pass - $wpToJoomlaMenuItemMap[$item['ID']] = $menuItemTable->id; + // Map the old Source ID to the new Joomla ID for the second pass + $sourceToJoomlaMenuItemMap[$item['ID']] = $menuItemTable->id; $counts['menu_items']++; } // Step 3: Second Pass - Import all CHILD menu items. - foreach ($wpMenuItems as $item) { + foreach ($sourceMenuItems as $item) { if ((string) ($item['menu_item_parent'] ?? '0') === '0') { continue; // Skip top-level items on this pass } - $wpParentId = $item['menu_item_parent']; + $sourceParentId = $item['menu_item_parent']; // Ensure the parent was successfully imported in the first pass - if (!isset($wpToJoomlaMenuItemMap[$wpParentId])) { - $result['errors'][] = sprintf('Skipping child item "%s" because its parent (WP ID: %s) was not found.', $item['title'], $wpParentId); + if (!isset($sourceToJoomlaMenuItemMap[$sourceParentId])) { + $result['errors'][] = sprintf('Skipping child item "%s" because its parent (WP ID: %s) was not found.', $item['title'], $sourceParentId); continue; } - $joomlaParentId = $wpToJoomlaMenuItemMap[$wpParentId]; + $joomlaParentId = $sourceToJoomlaMenuItemMap[$sourceParentId]; // Load the parent to get its level and path for the new child $parentTable = $menusMvcFactory->createTable('Menu', 'Administrator'); @@ -1467,7 +1468,7 @@ protected function processMenus(array $menus, array $contentMap, array &$counts) $alias = OutputFilter::stringURLSafe($item['title']); $menuItemData = [ - 'menutype' => $wpMenuName, + 'menutype' => $sourceMenuName, 'title' => $item['title'] ?? 'Untitled', 'alias' => $alias, 'path' => $parentTable->path . '/' . $alias, @@ -1488,34 +1489,34 @@ protected function processMenus(array $menus, array $contentMap, array &$counts) } // Also map this new child item in case it is a parent itself - $wpToJoomlaMenuItemMap[$item['ID']] = $menuItemTable->id; + $sourceToJoomlaMenuItemMap[$item['ID']] = $menuItemTable->id; $counts['menu_items']++; } } catch (\Exception $e) { - $result['errors'][] = sprintf('CRITICAL ERROR importing menu "%s": %s', $wpMenuName, $e->getMessage()); + $result['errors'][] = sprintf('CRITICAL ERROR importing menu "%s": %s', $sourceMenuName, $e->getMessage()); } } - $result['map'] = $wpToJoomlaMenuItemMap; + $result['map'] = $sourceToJoomlaMenuItemMap; return $result; } /** * Helper function to generate the correct Joomla link string and type. * - * @param array $wpItem A single WordPress menu item. + * @param array $sourceItem A single Source menu item. * @param array $contentMap The master content map. * * @return array An array containing [string $link, string $type]. */ - protected function generateJoomlaLink(array $wpItem, array $contentMap): array + protected function generateJoomlaLink(array $sourceItem, array $contentMap): array { - switch ($wpItem['object'] ?? 'custom') { + switch ($sourceItem['object'] ?? 'custom') { case 'page': case 'post': // Use the content map to find the new Joomla Article ID - $wpId = $wpItem['object_id'] ?? 0; - $joomlaId = $contentMap['posts'][$wpId] ?? 0; + $sourceId = $sourceItem['object_id'] ?? 0; + $joomlaId = $contentMap['posts'][$sourceId] ?? 0; if ($joomlaId) { return ['index.php?option=com_content&view=article&id=' . (int) $joomlaId, 'component']; @@ -1524,8 +1525,8 @@ protected function generateJoomlaLink(array $wpItem, array $contentMap): array case 'category': // Use the content map to find the new Joomla Category ID - $wpId = $wpItem['object_id'] ?? 0; - $joomlaId = $contentMap['categories'][$wpId] ?? 0; + $sourceId = $sourceItem['object_id'] ?? 0; + $joomlaId = $contentMap['categories'][$sourceId] ?? 0; if ($joomlaId) { return ['index.php?option=com_content&view=category&layout=blog&id=' . (int) $joomlaId, 'component']; @@ -1535,7 +1536,7 @@ protected function generateJoomlaLink(array $wpItem, array $contentMap): array case 'custom': default: // For custom links, just use the URL directly. - return [$wpItem['url'] ?? '#', 'url']; + return [$sourceItem['url'] ?? '#', 'url']; } // Fallback if the mapped content was not found diff --git a/src/component/cmsmigrator.xml b/src/component/cmsmigrator.xml index fc23116..98584a6 100644 --- a/src/component/cmsmigrator.xml +++ b/src/component/cmsmigrator.xml @@ -9,9 +9,9 @@ --> COM_CMSMIGRATOR - Rahul Singh - rahulsingh19390@gmail.com - https://rahulsingh.free.nf/ + Joomla Academy + + 2025-06-05 (C) 2025 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt @@ -22,11 +22,6 @@ - - - modules - - joomla.asset.json js diff --git a/src/component/modules/mod_migrationnotice/mod_migrationnotice.php b/src/component/modules/mod_migrationnotice/mod_migrationnotice.php deleted file mode 100644 index bd9e848..0000000 --- a/src/component/modules/mod_migrationnotice/mod_migrationnotice.php +++ /dev/null @@ -1,56 +0,0 @@ -get('show_notice', 1); -$noticeType = $params->get('notice_type', 'both'); -$alertType = $params->get('alert_type', 'info'); -$showResetLink = (bool) $params->get('show_reset_link', 1); -$customMessage = $params->get('custom_message', ''); - -// Only display if notice is enabled -if (!$showNotice) { - return; -} - -// Get application -$app = Factory::getApplication(); -$user = $app->getIdentity(); - -// Check if user is logged in (guest users see migration info, logged users see password reset info) -$isGuest = $user->guest; - -// Prepare data for template -$moduleData = new stdClass(); -$moduleData->showNotice = $showNotice; -$moduleData->noticeType = $noticeType; -$moduleData->alertType = $alertType; -$moduleData->showResetLink = $showResetLink; -$moduleData->customMessage = $customMessage; -$moduleData->isGuest = $isGuest; - -// Generate password reset URL -if ($showResetLink) { - $moduleData->resetUrl = Route::_('index.php?option=com_users&view=reset'); -} - -// Get login URL for guests -if ($isGuest) { - $moduleData->loginUrl = Route::_('index.php?option=com_users&view=login'); -} - -// Load template -require ModuleHelper::getLayoutPath('mod_migrationnotice', $params->get('layout', 'default')); diff --git a/src/component/script.php b/src/component/script.php index 0f1406e..81ac877 100644 --- a/src/component/script.php +++ b/src/component/script.php @@ -160,7 +160,7 @@ private function createMediaFolder(): void } /** - * Install bundled extensions that come with the component + * Placeholder for future bundled extensions installation * * @param InstallerAdapter $parent The installer adapter * @@ -168,43 +168,6 @@ private function createMediaFolder(): void */ private function installBundledExtensions($parent): void { - // Get the source path of the component package - $sourcePath = $parent->getParent()->getPath('source'); - - // Try different possible paths for the module - $possiblePaths = [ - $sourcePath . '/modules/mod_migrationnotice', - $sourcePath . '/../modules/mod_migrationnotice', - dirname($sourcePath) . '/modules/mod_migrationnotice' - ]; - - $moduleSourcePath = null; - foreach ($possiblePaths as $path) { - Log::add('Checking path: ' . $path, Log::INFO, 'com_cmsmigrator'); - if (file_exists($path) && file_exists($path . '/mod_migrationnotice.xml')) { - $moduleSourcePath = $path; - Log::add('Found module at: ' . $path, Log::INFO, 'com_cmsmigrator'); - break; - } - } - - if ($moduleSourcePath) { - try { - $moduleInstaller = new Installer(); - $result = $moduleInstaller->install($moduleSourcePath); - - if ($result) { - Log::add('Successfully installed bundled module: mod_migrationnotice', Log::INFO, 'com_cmsmigrator'); - } else { - $errors = $moduleInstaller->getErrors(); - Log::add('Failed to install bundled module: mod_migrationnotice. Errors: ' . implode('; ', $errors), Log::WARNING, 'com_cmsmigrator'); - } - } catch (\Exception $e) { - Log::add('Exception during module installation: ' . $e->getMessage(), Log::ERROR, 'com_cmsmigrator'); - } - } else { - Log::add('Bundled module not found in any of the expected locations', Log::WARNING, 'com_cmsmigrator'); - Log::add('Available files in source: ' . print_r(scandir($sourcePath), true), Log::INFO, 'com_cmsmigrator'); - } + // module installation code for bundled installer } } diff --git a/src/component/modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.ini b/src/modules/mod_migrationnotice/language/en-GB/mod_migrationnotice.ini similarity index 87% rename from src/component/modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.ini rename to src/modules/mod_migrationnotice/language/en-GB/mod_migrationnotice.ini index 234fc5c..68f6641 100644 --- a/src/component/modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.ini +++ b/src/modules/mod_migrationnotice/language/en-GB/mod_migrationnotice.ini @@ -1,21 +1,12 @@ ; Migration Notice Module Language File -; Copyright (C) 2025 Joomla Academy. All rights reserved. -; License GNU General Public License version 2 or later -; Module Description MOD_MIGRATIONNOTICE_XML_DESCRIPTION="A module that helps site administrators explain password resets to end users during and after migration from WordPress to Joomla. Provides clear information about the need to use Joomla's password reset feature due to security requirements when upgrading to a new platform." ; Basic Configuration MOD_MIGRATIONNOTICE_FIELDSET_BASIC_LABEL="Basic Settings" -MOD_MIGRATIONNOTICE_FIELD_SHOW_NOTICE_LABEL="Show Migration Notice" -MOD_MIGRATIONNOTICE_FIELD_SHOW_NOTICE_DESC="Display the migration notice to users." - -MOD_MIGRATIONNOTICE_FIELD_NOTICE_TYPE_LABEL="Notice Type" -MOD_MIGRATIONNOTICE_FIELD_NOTICE_TYPE_DESC="Choose what type of information to display to users." -MOD_MIGRATIONNOTICE_OPTION_MIGRATION_ONLY="Migration Information Only" -MOD_MIGRATIONNOTICE_OPTION_PASSWORD_ONLY="Password Reset Information Only" -MOD_MIGRATIONNOTICE_OPTION_BOTH="Both Migration and Password Information" +MOD_MIGRATIONNOTICE_FIELD_CUSTOM_MESSAGE_LABEL="Custom Message" +MOD_MIGRATIONNOTICE_FIELD_CUSTOM_MESSAGE_DESC="Optional custom message to override the default migration message. Leave empty to use default text." MOD_MIGRATIONNOTICE_FIELD_ALERT_TYPE_LABEL="Alert Style" MOD_MIGRATIONNOTICE_FIELD_ALERT_TYPE_DESC="Choose the visual style for the alert messages." @@ -24,27 +15,33 @@ MOD_MIGRATIONNOTICE_OPTION_WARNING="Warning (Yellow)" MOD_MIGRATIONNOTICE_OPTION_SUCCESS="Success (Green)" MOD_MIGRATIONNOTICE_OPTION_ERROR="Error (Red)" +MOD_MIGRATIONNOTICE_FIELD_NOTICE_TYPE_LABEL="Notice Type" +MOD_MIGRATIONNOTICE_FIELD_NOTICE_TYPE_DESC="Choose what type of information to display to users." +MOD_MIGRATIONNOTICE_OPTION_MIGRATION_ONLY="Migration Information Only" +MOD_MIGRATIONNOTICE_OPTION_PASSWORD_ONLY="Password Reset Information Only" +MOD_MIGRATIONNOTICE_OPTION_BOTH="Both Migration and Password Information" + +MOD_MIGRATIONNOTICE_FIELD_SHOW_NOTICE_LABEL="Show Migration Notice" +MOD_MIGRATIONNOTICE_FIELD_SHOW_NOTICE_DESC="Display the migration notice to users." + MOD_MIGRATIONNOTICE_FIELD_SHOW_RESET_LINK_LABEL="Show Password Reset Link" MOD_MIGRATIONNOTICE_FIELD_SHOW_RESET_LINK_DESC="Display a direct link to the password reset form." -MOD_MIGRATIONNOTICE_FIELD_CUSTOM_MESSAGE_LABEL="Custom Message" -MOD_MIGRATIONNOTICE_FIELD_CUSTOM_MESSAGE_DESC="Optional custom message to override the default migration message. Leave empty to use default text." - ; Frontend Messages +MOD_MIGRATIONNOTICE_ADMIN_TITLE="For Site Administrators" +MOD_MIGRATIONNOTICE_ADMIN_MESSAGE="Site Administrators need to contact the site owner to reset their passwords." + +MOD_MIGRATIONNOTICE_GUEST_INFO="If you had an account on our previous website, you'll need to reset your password before logging in." + MOD_MIGRATIONNOTICE_MIGRATION_TITLE="Website Migration Complete" MOD_MIGRATIONNOTICE_MIGRATION_MESSAGE="Welcome to our upgraded website! We've successfully migrated from WordPress to Joomla to provide you with a better, more secure experience." MOD_MIGRATIONNOTICE_PASSWORD_TITLE="Important: Password Reset Required" MOD_MIGRATIONNOTICE_PASSWORD_MESSAGE="For security reasons, your existing password cannot be transferred from the previous platform. Please use the password reset feature below to create a new password for your account." -MOD_MIGRATIONNOTICE_GUEST_INFO="If you had an account on our previous website, you'll need to reset your password before logging in." - -MOD_MIGRATIONNOTICE_ADMIN_TITLE="For Site Administrators" -MOD_MIGRATIONNOTICE_ADMIN_MESSAGE="Site Administrators need to contact the site owner to reset their passwords." - ; Buttons and Links MOD_MIGRATIONNOTICE_LOGIN_BUTTON="Login" MOD_MIGRATIONNOTICE_RESET_PASSWORD_BUTTON="Reset Password" -; Legacy compatibility (aligns with script.php overrides) -MOD_MIGRATION_NOTICE="Your site has been migrated from WordPress. For security, existing WordPress passwords cannot be used in Joomla. Please reset your password using the 'Forgot Password' link."OD_MIGRATION_NOTICE="Your site has been migrated from WordPress. For security, existing WordPress passwords cannot be used in Joomla. Please reset your password using the ‘Forgot Password’ link." +; Legacy compatibility / general notice +MOD_MIGRATION_NOTICE="Your site has been migrated from WordPress. For security, existing WordPress passwords cannot be used in Joomla. Please reset your password using the 'Forgot Password' link." diff --git a/src/component/modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.sys.ini b/src/modules/mod_migrationnotice/language/en-GB/mod_migrationnotice.sys.ini similarity index 100% rename from src/component/modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.sys.ini rename to src/modules/mod_migrationnotice/language/en-GB/mod_migrationnotice.sys.ini diff --git a/src/modules/mod_migrationnotice/media/css/module.css b/src/modules/mod_migrationnotice/media/css/module.css new file mode 100644 index 0000000..4fd8d39 --- /dev/null +++ b/src/modules/mod_migrationnotice/media/css/module.css @@ -0,0 +1,40 @@ +/** + * @package Migration Notice Module + * @subpackage mod_migrationnotice + * @copyright Copyright (C) 2025 Joomla Academy. All rights reserved. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +.migration-notice-module .alert { + margin-bottom: 1rem; +} + +.migration-notice-module .alert-heading { + margin-bottom: 0.75rem; + font-size: 1.1rem; +} + +.migration-notice-module .alert-heading .icon-info-circle, +.migration-notice-module .alert-heading .icon-key { + margin-right: 0.5rem; +} + +.migration-notice-module .btn { + margin-top: 0.5rem; +} + +.migration-notice-module .btn .icon-lock, +.migration-notice-module .btn .icon-refresh { + margin-right: 0.25rem; +} + +.migration-notice-module .admin-info { + border-left: 4px solid #17a2b8; +} + +@media (max-width: 576px) { + .migration-notice-module .btn { + width: 100%; + margin-top: 0.75rem; + } +} diff --git a/src/component/modules/mod_migrationnotice/mod_migrationnotice.xml b/src/modules/mod_migrationnotice/mod_migrationnotice.xml similarity index 84% rename from src/component/modules/mod_migrationnotice/mod_migrationnotice.xml rename to src/modules/mod_migrationnotice/mod_migrationnotice.xml index 01d985d..d376f79 100644 --- a/src/component/modules/mod_migrationnotice/mod_migrationnotice.xml +++ b/src/modules/mod_migrationnotice/mod_migrationnotice.xml @@ -1,24 +1,40 @@ + - mod_migrationnotice - Migration Notice Module + MOD_MIGRATIONNOTICE MOD_MIGRATIONNOTICE_XML_DESCRIPTION - 1.0.0 + 0.9.0 Joomla Academy + 2025-10-03 - Copyright (C) 2025 Joomla Academy. All rights reserved. + (C) 2025 Open Source Matters, Inc. GNU General Public License version 2 or later + Joomla\Module\MigrationNotice + - mod_migrationnotice.php + services + src tmpl language + media + + css + + - language/en-GB/en-GB.mod_migrationnotice.ini - language/en-GB/en-GB.mod_migrationnotice.sys.ini + language/en-GB/mod_migrationnotice.ini + language/en-GB/mod_migrationnotice.sys.ini script.php diff --git a/src/component/modules/mod_migrationnotice/script.php b/src/modules/mod_migrationnotice/script.php similarity index 100% rename from src/component/modules/mod_migrationnotice/script.php rename to src/modules/mod_migrationnotice/script.php diff --git a/src/modules/mod_migrationnotice/services/provider.php b/src/modules/mod_migrationnotice/services/provider.php new file mode 100644 index 0000000..a9c6d57 --- /dev/null +++ b/src/modules/mod_migrationnotice/services/provider.php @@ -0,0 +1,34 @@ +registerServiceProvider(new ModuleDispatcherFactoryServiceProvider('\\Joomla\\Module\\MigrationNotice')); + $container->registerServiceProvider(new HelperFactoryServiceProvider('\\Joomla\\Module\\MigrationNotice\\Site\\Helper')); + $container->registerServiceProvider(new ModuleServiceProvider()); + } +}; \ No newline at end of file diff --git a/src/modules/mod_migrationnotice/src/Dispatcher/Dispatcher.php b/src/modules/mod_migrationnotice/src/Dispatcher/Dispatcher.php new file mode 100644 index 0000000..a48c3c8 --- /dev/null +++ b/src/modules/mod_migrationnotice/src/Dispatcher/Dispatcher.php @@ -0,0 +1,60 @@ +get('show_notice', 1); + + if (!$showNotice) { + return $data; + } + + // Get the helper via HelperFactory + $helper = $this->getHelperFactory()->getHelper('MigrationNoticeHelper'); + + // Prepare module data + $moduleData = $helper->getModuleData($params, $this->getApplication()); + + // Add our custom data to the layout data + $data['moduleData'] = $moduleData; + + return $data; + } +} \ No newline at end of file diff --git a/src/modules/mod_migrationnotice/src/Helper/MigrationNoticeHelper.php b/src/modules/mod_migrationnotice/src/Helper/MigrationNoticeHelper.php new file mode 100644 index 0000000..3e0b6e2 --- /dev/null +++ b/src/modules/mod_migrationnotice/src/Helper/MigrationNoticeHelper.php @@ -0,0 +1,60 @@ +getIdentity(); + + // Prepare module data object + $moduleData = new \stdClass(); + $moduleData->showNotice = (bool) $params->get('show_notice', 1); + $moduleData->noticeType = $params->get('notice_type', 'both'); + $moduleData->alertType = $params->get('alert_type', 'info'); + $moduleData->showResetLink = (bool) $params->get('show_reset_link', 1); + $moduleData->customMessage = $params->get('custom_message', ''); + $moduleData->isGuest = $user->guest; + + // Generate URLs if needed + if ($moduleData->showResetLink) { + $moduleData->resetUrl = Route::_('index.php?option=com_users&view=reset'); + } + + if ($moduleData->isGuest) { + $moduleData->loginUrl = Route::_('index.php?option=com_users&view=login'); + } + + return $moduleData; + } +} \ No newline at end of file diff --git a/src/component/modules/mod_migrationnotice/tmpl/default.php b/src/modules/mod_migrationnotice/tmpl/default.php similarity index 79% rename from src/component/modules/mod_migrationnotice/tmpl/default.php rename to src/modules/mod_migrationnotice/tmpl/default.php index 174362a..253ea72 100644 --- a/src/component/modules/mod_migrationnotice/tmpl/default.php +++ b/src/modules/mod_migrationnotice/tmpl/default.php @@ -1,12 +1,13 @@ - - diff --git a/src/plugins/migration/json/json.xml b/src/plugins/migration/json/json.xml index bd18cb6..eca6609 100644 --- a/src/plugins/migration/json/json.xml +++ b/src/plugins/migration/json/json.xml @@ -1,7 +1,7 @@ plg_migration_json - Rahul Singh + Joomla Academy 2025-06-20 (C) 2025 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/src/plugins/migration/wordpress/wordpress.xml b/src/plugins/migration/wordpress/wordpress.xml index 3a945a7..bf8b656 100644 --- a/src/plugins/migration/wordpress/wordpress.xml +++ b/src/plugins/migration/wordpress/wordpress.xml @@ -11,7 +11,7 @@ PLG_MIGRATION_WORDPRESS 1.0.0 PLG_MIGRATION_WORDPRESS_XML_DESCRIPTION - Rahul Singh + Joomla Academy 2025-06-05 (C) 2025 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/tests/unit/Integration/ComponentPackageTest.php b/tests/unit/Integration/ComponentPackageTest.php index fea3304..79ea02b 100644 --- a/tests/unit/Integration/ComponentPackageTest.php +++ b/tests/unit/Integration/ComponentPackageTest.php @@ -123,13 +123,7 @@ private function verifyZipContents(string $zipPath): void 'media/com_cmsmigrator/css/admin.css', 'media/com_cmsmigrator/js/admin.js', 'media/com_cmsmigrator/js/init.js', - 'media/com_cmsmigrator/js/migration-form.js', - 'modules/mod_migrationnotice/mod_migrationnotice.php', - 'modules/mod_migrationnotice/mod_migrationnotice.xml', - 'modules/mod_migrationnotice/script.php', - 'modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.ini', - 'modules/mod_migrationnotice/language/en-GB/en-GB.mod_migrationnotice.sys.ini', - 'modules/mod_migrationnotice/tmpl/default.php' + 'media/com_cmsmigrator/js/migration-form.js' ]; foreach ($expectedFiles as $expectedFile) {