Home / Admin / WPCode Snippet: MU-Plugins Sync Engine
Duplicate Snippet

Embed Snippet on Your Site

WPCode Snippet: MU-Plugins Sync Engine

/**
* WPCode Snippet: MU-Plugins Sync Engine
* Description: Synchronizes MU-Plugins FROM /wp-content/mu-plugins/ TO wpcode_snippets_sync CPT
* Location: Run Everywhere
* Priority: 25
*
* INSTALL ORDER: #6 - Install AFTER the WPCode sync system is fully set up
*
* IMPORTANT:
* - This is an ADD-ON to the existing WPCode Snippets Sync system
* - Does NOT modify any existing snippets - completely standalone
* - Reuses the same wpcode_snippets_sync CPT
* - MU-Plugins are identified by source_type = 'mu-plugin'
* - WPCode snippets have source_type = 'wpcode' (set by updated sync engine)
*
* REQUIRES:
* - wpcode_snippets_sync CPT (from snippet #1)
* - ACF fields including: source_type, file_path (NEW fields from updated ACF snippet)
* - Existing sync engine continues to work unchanged
*
* BEST PRACTICES IMPLEMENTED (v1.2.0):
* ✅ Uses native get_mu_plugins() WordPress function (wp-admin/includes/plugin.php)
* ✅ Uses native get_file_data() for header parsing instead of custom regex
* ✅ Capability check (current_user_can) BEFORE nonce verification per WPCS
* ✅ Uses WPMU_PLUGIN_DIR constant (standard WordPress)
* ✅ Proper escaping for all output (esc_html, esc_attr, etc.)
* ✅ Uses wp_safe_redirect() for redirects
* ✅ Uses gmdate() instead of date() for timezone safety
* ✅ Proper nonce verification with wp_verify_nonce()
* ✅ Uses sanitize_text_field() + wp_unslash() for nonce sanitization per WPCS
* ✅ Uses wp_mkdir_p() for directory creation
* ✅ Uses wp_delete_file() for secure file deletion
* ✅ Uses no_found_rows for optimized queries
* ✅ file_get_contents() only for LOCAL files (acceptable per WP VIP docs)
*
* @version 1.2.0
* @since 2025-01-02
* @updated 2025-01-03 - Moved UI to CPT list page instead of separate Tools menu
*/

ismail daugherty PRO
<10
Code Preview
php
<?php
/**
 * WPCode Snippet: MU-Plugins Sync Engine
 * Description: Synchronizes MU-Plugins FROM /wp-content/mu-plugins/ TO wpcode_snippets_sync CPT
 * Location: Run Everywhere
 * Priority: 25
 * 
 * INSTALL ORDER: #6 - Install AFTER the WPCode sync system is fully set up
 * 
 * IMPORTANT: 
 *   - This is an ADD-ON to the existing WPCode Snippets Sync system
 *   - Does NOT modify any existing snippets - completely standalone
 *   - Reuses the same wpcode_snippets_sync CPT
 *   - MU-Plugins are identified by source_type = 'mu-plugin'
 *   - Subfolder files are identified by source_type = 'mu-plugin-subfolder'
 *   - WPCode snippets have source_type = 'wpcode' (set by updated sync engine)
 * 
 * REQUIRES:
 *   - wpcode_snippets_sync CPT (from snippet #1)
 *   - ACF fields including: source_type, file_path (NEW fields from updated ACF snippet)
 *   - Existing sync engine continues to work unchanged
 * 
 * BEST PRACTICES IMPLEMENTED (v1.4.0):
 *   ✅ Uses native get_mu_plugins() WordPress function (wp-admin/includes/plugin.php)
 *   ✅ Uses native get_file_data() for header parsing instead of custom regex
 *   ✅ Capability check (current_user_can) BEFORE nonce verification per WPCS
 *   ✅ Uses WPMU_PLUGIN_DIR constant (standard WordPress)
 *   ✅ Proper escaping for all output (esc_html, esc_attr, etc.)
 *   ✅ Uses wp_safe_redirect() for redirects
 *   ✅ Uses gmdate() instead of date() for timezone safety
 *   ✅ Proper nonce verification with wp_verify_nonce()
 *   ✅ Uses sanitize_text_field() + wp_unslash() for nonce sanitization per WPCS
 *   ✅ Uses wp_mkdir_p() for directory creation
 *   ✅ Uses wp_delete_file() for secure file deletion
 *   ✅ Uses no_found_rows for optimized queries
 *   ✅ file_get_contents() only for LOCAL files (acceptable per WP VIP docs)
 *   ✅ realpath() validation for directory traversal prevention
 * 
 * v1.5.0 UPDATES:
 *   - Added "Sync WPCode Now" button to control panel
 *   - All sync controls now in one place on CPT list page
 *   - Calls kic_sync_all_wpcode_snippets() from AD_SYNC snippet
 *   - Shows WPCode active/inactive counts
 * 
 * v1.4.0 UPDATES:
 *   - Now scans ALL mu-plugins subfolders (not just "shortcode" folders)
 *   - Renamed function: kic_scan_mu_subfolders() -> kic_scan_mu_subfolders()
 *   - UI label changed from "Subfolders" to "Subfolders"
 * 
 * v1.3.0 UPDATES:
 *   - Added shortcode subfolder scanning and syncing
 *   - Scans mu-plugins subfolders containing "shortcode" in name
 *   - New source_type = 'mu-plugin-subfolder' for subfolder files
 *   - UI shows shortcode folder counts
 *   - Cleanup handles both root mu-plugins and subfolder files
 * 
 * @version 1.5.0
 * @since 2025-01-02
 * @updated 2025-01-25 - Added WPCode sync button to control panel
 */
defined( 'ABSPATH' ) || exit;
/**
 * Get the MU-Plugins directory path
 * Uses WordPress constant WPMU_PLUGIN_DIR
 * 
 * @return string Full path to mu-plugins directory
 */
function kic_get_mu_plugins_dir() {
    return WPMU_PLUGIN_DIR;
}
/**
 * Scan MU-Plugins directory and return array of plugin files with data
 * Uses native WordPress get_mu_plugins() function for reliability
 * 
 * @return array Array of mu-plugin data arrays
 */
function kic_scan_mu_plugins() {
    // Include the plugin.php file if not already loaded (needed for get_mu_plugins)
    if ( ! function_exists( 'get_mu_plugins' ) ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
    }
    
    $mu_plugins_dir = kic_get_mu_plugins_dir();
    $plugins = array();
    
    if ( ! is_dir( $mu_plugins_dir ) ) {
        return $plugins;
    }
    
    // Use native WordPress function - returns array keyed by filename
    // Each value is array with Name, PluginURI, Version, Description, Author, AuthorURI, TextDomain, DomainPath, Network
    $native_plugins = get_mu_plugins();
    
    foreach ( $native_plugins as $filename => $plugin_data ) {
        $file_path = trailingslashit( $mu_plugins_dir ) . $filename;
        
        $plugins[] = array(
            'file'       => $filename,
            'path'       => $file_path,
            'basename'   => basename( $filename, '.php' ),
            'data'       => $plugin_data, // Native WordPress plugin data
        );
    }
    
    return $plugins;
}
/**
 * Scan mu-plugins directory for ALL subfolders
 * Returns array of folder info with their PHP files
 * Skips nested subdirectories (like OLD/) - only gets root-level .php files in each folder
 * 
 * @return array Array of subfolder data
 */
if ( ! function_exists( 'kic_scan_mu_subfolders' ) ) {
function kic_scan_mu_subfolders() {
    $mu_plugins_dir = kic_get_mu_plugins_dir();
    $subfolders = array();
    
    if ( ! is_dir( $mu_plugins_dir ) ) {
        return $subfolders;
    }
    
    // Scan for subdirectories
    $directories = glob( trailingslashit( $mu_plugins_dir ) . '*', GLOB_ONLYDIR );
    
    if ( empty( $directories ) ) {
        return $subfolders;
    }
    
    foreach ( $directories as $dir_path ) {
        $folder_name = basename( $dir_path );
        
        // Get all PHP files in this folder (not recursive into subdirs like OLD/)
        $php_files = glob( trailingslashit( $dir_path ) . '*.php' );
        
        if ( empty( $php_files ) ) {
            continue;
        }
        
        $files_data = array();
        foreach ( $php_files as $file_path ) {
            $files_data[] = array(
                'path'        => $file_path,
                'filename'    => basename( $file_path ),
                'folder_name' => $folder_name,
            );
        }
        
        $subfolders[] = array(
            'folder_name' => $folder_name,
            'folder_path' => $dir_path,
            'files'       => $files_data,
            'file_count'  => count( $files_data ),
        );
    }
    
    return $subfolders;
}
}
/**
 * Get plugin header data using native WordPress function
 * Falls back to get_file_data() which is the core function used by get_plugin_data()
 * 
 * @param string $file_path Full path to the plugin file
 * @return array|false Plugin data array or false on failure
 */
function kic_get_mu_plugin_data( $file_path ) {
    if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
        return false;
    }
    
    // Use native WordPress get_file_data() function
    // This is the same function used by get_plugin_data() internally
    // It reads only first 8KB of file for efficiency
    $default_headers = array(
        'Name'        => 'Plugin Name',
        'PluginURI'   => 'Plugin URI',
        'Version'     => 'Version',
        'Description' => 'Description',
        'Author'      => 'Author',
        'AuthorURI'   => 'Author URI',
        'TextDomain'  => 'Text Domain',
        'DomainPath'  => 'Domain Path',
        'Network'     => 'Network',
        'RequiresWP'  => 'Requires at least',
        'RequiresPHP' => 'Requires PHP',
    );
    
    $plugin_data = get_file_data( $file_path, $default_headers, 'plugin' );
    
    // If no plugin name found, use filename
    if ( empty( $plugin_data['Name'] ) ) {
        $plugin_data['Name'] = basename( $file_path, '.php' );
    }
    
    return $plugin_data;
}
/**
 * Sync a single MU-Plugin to the sync CPT
 * 
 * @param array $plugin_info Plugin info array from kic_scan_mu_plugins()
 * @return int|false Sync post ID on success, false on failure
 */
function kic_sync_mu_plugin( $plugin_info ) {
    $file_path = $plugin_info['path'];
    $file_name = $plugin_info['file'];
    
    // Use pre-fetched data if available, otherwise get fresh
    $plugin_data = isset( $plugin_info['data'] ) ? $plugin_info['data'] : kic_get_mu_plugin_data( $file_path );
    
    if ( ! $plugin_data ) {
        return false;
    }
    
    // Read full file content for sync
    // Note: file_get_contents() is acceptable for LOCAL files per WordPress VIP docs
    // wp_remote_get() is only required for REMOTE URLs
    $code_content = file_get_contents( $file_path );
    
    if ( false === $code_content ) {
        return false;
    }
    
    // Generate unique identifier for this MU-plugin
    $kic_id = 'mu_' . sanitize_key( $plugin_info['basename'] );
    
    // Check if sync post already exists for this MU-plugin
    $existing_sync_post = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'meta_key'       => 'kic_snippet_id',
        'meta_value'     => $kic_id,
        'posts_per_page' => 1,
        'fields'         => 'ids',
        'no_found_rows'  => true, // Performance optimization
    ) );
    
    $sync_post_id = ! empty( $existing_sync_post ) ? $existing_sync_post[0] : 0;
    
    // Prepare sync post data
    $sync_post_data = array(
        'post_title'   => sanitize_text_field( $plugin_data['Name'] ),
        'post_type'    => 'wpcode_snippets_sync',
        'post_status'  => 'publish',
        'post_author'  => get_current_user_id(),
    );
    
    // Update or insert sync post
    if ( $sync_post_id ) {
        $sync_post_data['ID'] = $sync_post_id;
        $result = wp_update_post( $sync_post_data, true );
    } else {
        $result = wp_insert_post( $sync_post_data, true );
    }
    
    if ( is_wp_error( $result ) ) {
        return false;
    }
    
    $sync_post_id = $result;
    
    // ==========================================
    // Update ACF fields
    // ==========================================
    
    // Source identification (NEW fields)
    update_field( 'source_type', 'mu-plugin', $sync_post_id );
    update_field( 'file_path', $file_path, $sync_post_id );
    
    // Core identifiers
    update_field( 'original_wpcode_id', '', $sync_post_id ); // Empty for MU-plugins
    update_field( 'kic_snippet_id', $kic_id, $sync_post_id );
    update_field( 'snippet_active', true, $sync_post_id ); // MU-plugins are always active
    
    // Snippet info
    update_field( 'snippet_title', sanitize_text_field( $plugin_data['Name'] ), $sync_post_id );
    update_field( 'snippet_type', 'php', $sync_post_id );
    update_field( 'snippet_location', 'mu-plugins', $sync_post_id );
    update_field( 'snippet_priority', 0, $sync_post_id ); // MU-plugins load first
    
    // Code content
    update_field( 'snippet_code', $code_content, $sync_post_id );
    
    // Create downloadable code file
    $attachment_id = kic_create_mu_plugin_file_attachment( $code_content, $plugin_data['Name'], $sync_post_id );
    if ( $attachment_id ) {
        update_field( 'snippet_code_file', $attachment_id, $sync_post_id );
    }
    
    // Metadata from plugin headers
    update_field( 'snippet_description', sanitize_textarea_field( $plugin_data['Description'] ), $sync_post_id );
    update_field( 'snippet_version', sanitize_text_field( $plugin_data['Version'] ), $sync_post_id );
    update_field( 'snippet_author', sanitize_text_field( $plugin_data['Author'] ), $sync_post_id );
    
    // File dates - use gmdate() for timezone safety
    $file_created  = filectime( $file_path );
    $file_modified = filemtime( $file_path );
    
    update_field( 'snippet_created_date', $file_created ? gmdate( 'Y-m-d H:i:s', $file_created ) : '', $sync_post_id );
    update_field( 'snippet_modified_date', $file_modified ? gmdate( 'Y-m-d H:i:s', $file_modified ) : '', $sync_post_id );
    
    // Sync timestamp
    update_field( 'last_sync_time', current_time( 'mysql' ), $sync_post_id );
    
    // Tags - combine version and requirements
    $tags = array( 'mu-plugin' );
    if ( ! empty( $plugin_data['Version'] ) ) {
        $tags[] = 'v' . sanitize_text_field( $plugin_data['Version'] );
    }
    if ( ! empty( $plugin_data['RequiresPHP'] ) ) {
        $tags[] = 'php-' . sanitize_text_field( $plugin_data['RequiresPHP'] );
    }
    update_field( 'snippet_tags', implode( ', ', $tags ), $sync_post_id );
    
    // Dependencies (from Requires PHP / Requires WP)
    $dependencies = array();
    if ( ! empty( $plugin_data['RequiresWP'] ) ) {
        $dependencies[] = 'WordPress ' . sanitize_text_field( $plugin_data['RequiresWP'] ) . '+';
    }
    if ( ! empty( $plugin_data['RequiresPHP'] ) ) {
        $dependencies[] = 'PHP ' . sanitize_text_field( $plugin_data['RequiresPHP'] ) . '+';
    }
    update_field( 'snippet_dependencies', implode( ', ', $dependencies ), $sync_post_id );
    
    // Settings that don't apply to MU-plugins
    update_field( 'snippet_auto_insert', false, $sync_post_id );
    update_field( 'snippet_device_type', 'all', $sync_post_id );
    update_field( 'snippet_schedule_start', '', $sync_post_id );
    update_field( 'snippet_schedule_end', '', $sync_post_id );
    
    return $sync_post_id;
}
/**
 * Sync a single mu-plugins subfolder file to the sync CPT
 * 
 * @param array $file_info File info array from kic_scan_mu_subfolders()
 * @return int|false Sync post ID on success, false on failure
 */
function kic_sync_subfolder_file( $file_info ) {
    $file_path   = $file_info['path'];
    $filename    = $file_info['filename'];
    $folder_name = $file_info['folder_name'];
    
    // Security: Verify file is within mu-plugins directory
    $mu_plugins_dir = kic_get_mu_plugins_dir();
    $real_path = realpath( $file_path );
    $real_mu_dir = realpath( $mu_plugins_dir );
    
    if ( ! $real_path || strpos( $real_path, $real_mu_dir ) !== 0 ) {
        return false;
    }
    
    if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
        return false;
    }
    
    // Get file data (plugin headers if present)
    $plugin_data = kic_get_mu_plugin_data( $file_path );
    
    // Read full file content
    $code_content = file_get_contents( $file_path );
    
    if ( false === $code_content ) {
        return false;
    }
    
    // Generate unique identifier: mu_subfolder_foldername_filename
    $basename = basename( $filename, '.php' );
    $kic_id = 'mu_subfolder_' . sanitize_key( $folder_name ) . '_' . sanitize_key( $basename );
    
    // Check if sync post already exists
    $existing_sync_post = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'meta_key'       => 'kic_snippet_id',
        'meta_value'     => $kic_id,
        'posts_per_page' => 1,
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ) );
    
    $sync_post_id = ! empty( $existing_sync_post ) ? $existing_sync_post[0] : 0;
    
    // Use plugin name if available, otherwise folder/filename
    $title = ! empty( $plugin_data['Name'] ) && $plugin_data['Name'] !== $basename 
        ? $plugin_data['Name'] 
        : $folder_name . '/' . $filename;
    
    // Prepare sync post data
    $sync_post_data = array(
        'post_title'   => sanitize_text_field( $title ),
        'post_type'    => 'wpcode_snippets_sync',
        'post_status'  => 'publish',
        'post_author'  => get_current_user_id(),
    );
    
    // Update or insert
    if ( $sync_post_id ) {
        $sync_post_data['ID'] = $sync_post_id;
        $result = wp_update_post( $sync_post_data, true );
    } else {
        $result = wp_insert_post( $sync_post_data, true );
    }
    
    if ( is_wp_error( $result ) ) {
        return false;
    }
    
    $sync_post_id = $result;
    
    // ==========================================
    // Update ACF fields
    // ==========================================
    
    // Source identification - use distinct type for subfolder files
    update_field( 'source_type', 'mu-plugin-subfolder', $sync_post_id );
    update_field( 'file_path', $file_path, $sync_post_id );
    
    // Core identifiers
    update_field( 'original_wpcode_id', '', $sync_post_id );
    update_field( 'kic_snippet_id', $kic_id, $sync_post_id );
    update_field( 'snippet_active', true, $sync_post_id ); // Always active
    
    // Snippet info
    update_field( 'snippet_title', sanitize_text_field( $title ), $sync_post_id );
    update_field( 'snippet_type', 'php', $sync_post_id );
    update_field( 'snippet_location', 'mu-plugins/' . $folder_name, $sync_post_id );
    update_field( 'snippet_priority', 0, $sync_post_id );
    
    // Code content
    update_field( 'snippet_code', $code_content, $sync_post_id );
    
    // Create downloadable code file
    $attachment_id = kic_create_mu_plugin_file_attachment( $code_content, $folder_name . '-' . $basename, $sync_post_id );
    if ( $attachment_id ) {
        update_field( 'snippet_code_file', $attachment_id, $sync_post_id );
    }
    
    // Metadata from plugin headers (if present)
    $description = ! empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : 'Shortcode file from ' . $folder_name;
    update_field( 'snippet_description', sanitize_textarea_field( $description ), $sync_post_id );
    update_field( 'snippet_version', sanitize_text_field( $plugin_data['Version'] ?? '' ), $sync_post_id );
    update_field( 'snippet_author', sanitize_text_field( $plugin_data['Author'] ?? '' ), $sync_post_id );
    
    // File dates
    $file_created  = filectime( $file_path );
    $file_modified = filemtime( $file_path );
    
    update_field( 'snippet_created_date', $file_created ? gmdate( 'Y-m-d H:i:s', $file_created ) : '', $sync_post_id );
    update_field( 'snippet_modified_date', $file_modified ? gmdate( 'Y-m-d H:i:s', $file_modified ) : '', $sync_post_id );
    
    // Sync timestamp
    update_field( 'last_sync_time', current_time( 'mysql' ), $sync_post_id );
    
    // Tags
    $tags = array( 'mu-plugin-subfolder', sanitize_key( $folder_name ) );
    update_field( 'snippet_tags', implode( ', ', $tags ), $sync_post_id );
    
    // No dependencies for subfolder files
    update_field( 'snippet_dependencies', '', $sync_post_id );
    
    // Settings that don't apply
    update_field( 'snippet_auto_insert', false, $sync_post_id );
    update_field( 'snippet_device_type', 'all', $sync_post_id );
    update_field( 'snippet_schedule_start', '', $sync_post_id );
    update_field( 'snippet_schedule_end', '', $sync_post_id );
    
    return $sync_post_id;
}
/**
 * Create a downloadable attachment for the code file
 * 
 * @param string $code_content The code content
 * @param string $plugin_name  The plugin name for filename
 * @param int    $parent_id    The parent post ID
 * @return int|false Attachment ID on success, false on failure
 */
function kic_create_mu_plugin_file_attachment( $code_content, $plugin_name, $parent_id ) {
    // Generate safe filename
    $safe_name = sanitize_file_name( $plugin_name );
    if ( empty( $safe_name ) ) {
        $safe_name = 'mu-plugin-' . $parent_id;
    }
    $filename = $safe_name . '.txt';
    
    // Get uploads directory
    $upload_dir = wp_upload_dir();
    $subdir = 'wpcode-sync-files';
    $target_dir = trailingslashit( $upload_dir['basedir'] ) . $subdir;
    
    // Create directory if needed - use wp_mkdir_p() which is WordPress standard
    if ( ! file_exists( $target_dir ) ) {
        wp_mkdir_p( $target_dir );
    }
    
    // Make filename unique
    $file_path = trailingslashit( $target_dir ) . wp_unique_filename( $target_dir, $filename );
    
    // Write file using WP_Filesystem would be ideal, but file_put_contents is acceptable
    // for admin-only operations in the uploads directory
    $bytes_written = file_put_contents( $file_path, $code_content );
    
    if ( false === $bytes_written ) {
        return false;
    }
    
    // Prepare attachment data
    $file_type = wp_check_filetype( basename( $file_path ), null );
    
    $attachment = array(
        'guid'           => trailingslashit( $upload_dir['baseurl'] ) . $subdir . '/' . basename( $file_path ),
        'post_mime_type' => $file_type['type'] ? $file_type['type'] : 'text/plain',
        'post_title'     => sanitize_file_name( basename( $file_path ) ),
        'post_content'   => '',
        'post_status'    => 'inherit',
    );
    
    // Insert attachment
    $attachment_id = wp_insert_attachment( $attachment, $file_path, $parent_id );
    
    if ( is_wp_error( $attachment_id ) ) {
        // Clean up the file we created - use wp_delete_file for security
        wp_delete_file( $file_path );
        return false;
    }
    
    // Generate attachment metadata
    require_once ABSPATH . 'wp-admin/includes/image.php';
    $attach_data = wp_generate_attachment_metadata( $attachment_id, $file_path );
    wp_update_attachment_metadata( $attachment_id, $attach_data );
    
    return $attachment_id;
}
/**
 * Sync ALL MU-Plugins (root files + all subfolders)
 * 
 * @return array Result array with success status, message, count, and errors
 */
function kic_sync_all_mu_plugins() {
    $mu_plugins = kic_scan_mu_plugins();
    $mu_subfolders = kic_scan_mu_subfolders();
    
    $synced_root = 0;
    $synced_subfolder = 0;
    $errors = array();
    
    // Sync root MU-plugins
    foreach ( $mu_plugins as $plugin ) {
        $result = kic_sync_mu_plugin( $plugin );
        
        if ( $result ) {
            $synced_root++;
        } else {
            $errors[] = $plugin['file'];
        }
    }
    
    // Sync all subfolder files
    foreach ( $mu_subfolders as $folder ) {
        foreach ( $folder['files'] as $file_info ) {
            $result = kic_sync_subfolder_file( $file_info );
            
            if ( $result ) {
                $synced_subfolder++;
            } else {
                $errors[] = $folder['folder_name'] . '/' . $file_info['filename'];
            }
        }
    }
    
    // Clean up orphaned sync posts
    kic_cleanup_deleted_mu_plugins( $mu_plugins, $mu_subfolders );
    
    $total = $synced_root + $synced_subfolder;
    
    return array(
        'success'          => true,
        'message'          => sprintf( 
            'Synced %d root MU-plugins + %d subfolder files (%d total).', 
            $synced_root,
            $synced_subfolder,
            $total
        ),
        'synced_root'      => $synced_root,
        'synced_subfolder' => $synced_subfolder,
        'synced'           => $total,
        'errors'           => $errors,
    );
}
/**
 * Clean up sync posts for MU-plugins and subfolder files that no longer exist
 * 
 * @param array $current_plugins    Array of currently existing root plugins
 * @param array $mu_subfolders  Array of currently existing all subfolders
 */
function kic_cleanup_deleted_mu_plugins( $current_plugins, $mu_subfolders = array() ) {
    // Get all current MU-plugin KIC IDs (root)
    $current_ids = array();
    foreach ( $current_plugins as $plugin ) {
        $current_ids[] = 'mu_' . sanitize_key( $plugin['basename'] );
    }
    
    // Add subfolder IDs
    foreach ( $mu_subfolders as $folder ) {
        foreach ( $folder['files'] as $file_info ) {
            $basename = basename( $file_info['filename'], '.php' );
            $current_ids[] = 'mu_subfolder_' . sanitize_key( $folder['folder_name'] ) . '_' . sanitize_key( $basename );
        }
    }
    
    // Find sync posts for MU-plugins/subfolders that no longer exist
    $sync_posts = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'meta_query'     => array(
            'relation' => 'OR',
            array(
                'key'   => 'source_type',
                'value' => 'mu-plugin',
            ),
            array(
                'key'   => 'source_type',
                'value' => 'mu-plugin-subfolder',
            ),
        ),
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ) );
    
    foreach ( $sync_posts as $sync_post_id ) {
        $kic_id = get_field( 'kic_snippet_id', $sync_post_id );
        
        if ( ! in_array( $kic_id, $current_ids, true ) ) {
            // Delete the code file attachment first
            $code_file = get_field( 'snippet_code_file', $sync_post_id );
            if ( $code_file && is_array( $code_file ) && ! empty( $code_file['ID'] ) ) {
                wp_delete_attachment( $code_file['ID'], true );
            }
            
            // Delete the sync post
            wp_delete_post( $sync_post_id, true );
        }
    }
}
/**
 * Add MU-Plugins sync box to the wpcode_snippets_sync CPT list page
 * Displays above the posts table for easy access
 */
add_action( 'admin_notices', function() {
    $screen = get_current_screen();
    
    // Only on the wpcode_snippets_sync CPT list page
    if ( ! $screen || 'edit-wpcode_snippets_sync' !== $screen->id ) {
        return;
    }
    
    // Capability check - admin only
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    
    $mu_plugins = kic_scan_mu_plugins();
    $mu_count = count( $mu_plugins );
    
    // Get all subfolders
    $mu_subfolders = kic_scan_mu_subfolders();
    $subfolder_count = count( $mu_subfolders );
    $subfolder_file_count = 0;
    foreach ( $mu_subfolders as $folder ) {
        $subfolder_file_count += $folder['file_count'];
    }
    
    // Count synced MU-plugins (root)
    $synced_mu = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'meta_query'     => array(
            array(
                'key'   => 'source_type',
                'value' => 'mu-plugin',
            ),
        ),
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ) );
    $synced_mu_count = count( $synced_mu );
    
    // Count synced subfolder files
    $synced_subfolder = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'meta_query'     => array(
            array(
                'key'   => 'source_type',
                'value' => 'mu-plugin-subfolder',
            ),
        ),
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ) );
    $synced_subfolder_count = count( $synced_subfolder );
    
    // Count synced WPCode snippets
    $synced_wpcode = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'meta_query'     => array(
            'relation' => 'OR',
            array(
                'key'   => 'source_type',
                'value' => 'wpcode',
            ),
            array(
                'key'     => 'source_type',
                'compare' => 'NOT EXISTS',
            ),
        ),
        'fields'         => 'ids',
        'no_found_rows'  => true,
    ) );
    $synced_wpcode_count = count( $synced_wpcode );
    
    ?>
    <div class="kic-sync-controls" style="background: #fff; border: 1px solid #ccd0d4; border-left: 4px solid #2271b1; padding: 15px 20px; margin: 15px 0; box-shadow: 0 1px 1px rgba(0,0,0,.04);">
        <div style="display: flex; flex-wrap: wrap; gap: 30px; align-items: flex-start;">
            
            <!-- Stats Section -->
            <div style="flex: 1; min-width: 200px;">
                <h3 style="margin: 0 0 10px 0; font-size: 14px;">📊 Sync Status</h3>
                <table class="widefat striped" style="width: auto; min-width: 320px;">
                    <tr>
                        <td><strong>WPCode Snippets:</strong></td>
                        <td><?php echo esc_html( $synced_wpcode_count ); ?> synced</td>
                    </tr>
                    <tr>
                        <td><strong>MU-Plugins (root):</strong></td>
                        <td><?php echo esc_html( $synced_mu_count ); ?> / <?php echo esc_html( $mu_count ); ?> synced</td>
                    </tr>
                    <tr>
                        <td><strong>Subfolders:</strong></td>
                        <td><?php echo esc_html( $synced_subfolder_count ); ?> / <?php echo esc_html( $subfolder_file_count ); ?> files (<?php echo esc_html( $subfolder_count ); ?> folders)</td>
                    </tr>
                    <tr>
                        <td><strong>MU-Plugins Dir:</strong></td>
                        <td><code style="font-size: 11px;"><?php echo esc_html( basename( kic_get_mu_plugins_dir() ) ); ?>/</code></td>
                    </tr>
                </table>
            </div>
            
            <!-- MU-Plugins Sync Button -->
            <div style="flex: 0 0 auto;">
                <h3 style="margin: 0 0 10px 0; font-size: 14px;">🔌 MU-Plugins</h3>
                <form method="post" action="" style="margin: 0;">
                    <?php wp_nonce_field( 'kic_sync_mu_plugins_action', 'kic_mu_nonce' ); ?>
                    <button type="submit" name="kic_sync_mu_plugins" class="button button-primary">
                        🔄 Sync MU-Plugins Now
                    </button>
                </form>
                <p style="margin: 8px 0 0 0; font-size: 12px; color: #666;">
                    <?php echo esc_html( $mu_count ); ?> root + <?php echo esc_html( $subfolder_file_count ); ?> subfolder files
                </p>
            </div>
            
            <!-- WPCode Sync Button -->
            <div style="flex: 0 0 auto;">
                <h3 style="margin: 0 0 10px 0; font-size: 14px;">📝 WPCode Snippets</h3>
                <form method="post" action="" style="margin: 0;">
                    <?php wp_nonce_field( 'kic_sync_wpcode_snippets_action', 'kic_wpcode_nonce' ); ?>
                    <button type="submit" name="kic_sync_wpcode_snippets" class="button button-secondary">
                        🔄 Sync WPCode Now
                    </button>
                </form>
                <?php
                $wpcode_total = wp_count_posts( 'wpcode' );
                $wpcode_source_count = isset( $wpcode_total->publish ) ? $wpcode_total->publish : 0;
                $wpcode_draft_count = isset( $wpcode_total->draft ) ? $wpcode_total->draft : 0;
                ?>
                <p style="margin: 8px 0 0 0; font-size: 12px; color: #666;">
                    <?php echo esc_html( $wpcode_source_count ); ?> active + <?php echo esc_html( $wpcode_draft_count ); ?> inactive
                </p>
            </div>
            
            <!-- MU-Plugins List (Expandable) -->
            <?php if ( $mu_count > 0 || $subfolder_count > 0 ) : ?>
            <div style="flex: 1; min-width: 250px;">
                <?php if ( $mu_count > 0 ) : ?>
                <details style="margin-bottom: 10px;">
                    <summary style="cursor: pointer; font-weight: bold; font-size: 14px; margin-bottom: 8px;">
                        📁 Root MU-Plugins (<?php echo esc_html( $mu_count ); ?>)
                    </summary>
                    <ul style="margin: 8px 0 0 15px; padding: 0; list-style: disc; font-size: 12px; max-height: 100px; overflow-y: auto;">
                        <?php foreach ( $mu_plugins as $plugin ) : 
                            $data = isset( $plugin['data'] ) ? $plugin['data'] : kic_get_mu_plugin_data( $plugin['path'] );
                            $name = ! empty( $data['Name'] ) ? $data['Name'] : $plugin['file'];
                        ?>
                        <li style="margin-bottom: 4px;">
                            <strong><?php echo esc_html( $name ); ?></strong>
                            <?php if ( ! empty( $data['Version'] ) ) : ?>
                                <span style="color: #666;">(v<?php echo esc_html( $data['Version'] ); ?>)</span>
                            <?php endif; ?>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                </details>
                <?php endif; ?>
                
                <?php if ( $subfolder_count > 0 ) : ?>
                <details>
                    <summary style="cursor: pointer; font-weight: bold; font-size: 14px; margin-bottom: 8px;">
                        📂 Subfolders (<?php echo esc_html( $subfolder_count ); ?>)
                    </summary>
                    <ul style="margin: 8px 0 0 15px; padding: 0; list-style: disc; font-size: 12px; max-height: 100px; overflow-y: auto;">
                        <?php foreach ( $mu_subfolders as $folder ) : ?>
                        <li style="margin-bottom: 4px;">
                            <strong><?php echo esc_html( $folder['folder_name'] ); ?>/</strong>
                            <span style="color: #666;">(<?php echo esc_html( $folder['file_count'] ); ?> files)</span>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                </details>
                <?php endif; ?>
            </div>
            <?php endif; ?>
            
        </div>
        
        <p style="margin: 12px 0 0 0; font-size: 11px; color: #888;">
            💡 <strong>Source Types:</strong> 
            <code>wpcode</code> = WPCode snippets | 
            <code>mu-plugin</code> = Root MU-plugins | 
            <code>mu-plugin-subfolder</code> = Subfolder files
        </p>
    </div>
    <?php
}, 5 ); // Priority 5 to show before other notices
/**
 * Handle MU-Plugins sync form submission
 * BEST PRACTICE: Capability check BEFORE nonce verification (per WPCS/Patchstack)
 */
add_action( 'admin_init', function() {
    // Check if form was submitted
    if ( ! isset( $_POST['kic_sync_mu_plugins'] ) ) {
        return;
    }
    
    // BEST PRACTICE: Capability check FIRST (per WPCS/Patchstack)
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 
            esc_html__( 'You do not have permission to sync MU-plugins.', 'kic-wpcode-sync' ),
            esc_html__( 'Permission Denied', 'kic-wpcode-sync' ),
            array( 'response' => 403 )
        );
    }
    
    // THEN nonce verification - sanitize_text_field + wp_unslash per WPCS
    // Reference: https://developer.wordpress.org/news/2023/08/understand-and-use-wordpress-nonces-properly/
    if ( ! isset( $_POST['kic_mu_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kic_mu_nonce'] ) ), 'kic_sync_mu_plugins_action' ) ) {
        wp_die( 
            esc_html__( 'Security check failed. Please try again.', 'kic-wpcode-sync' ),
            esc_html__( 'Security Error', 'kic-wpcode-sync' ),
            array( 'response' => 403 )
        );
    }
    
    // Perform sync
    $result = kic_sync_all_mu_plugins();
    
    // Store result for display
    set_transient( 'kic_mu_sync_result', $result, 30 );
    
    // Redirect to avoid form resubmission - use wp_safe_redirect
    $redirect_url = wp_get_referer();
    if ( ! $redirect_url ) {
        $redirect_url = admin_url( 'edit.php?post_type=wpcode_snippets_sync' );
    }
    
    wp_safe_redirect( add_query_arg( 'mu_synced', '1', $redirect_url ) );
    exit;
} );
/**
 * Display MU-Plugins sync result notice
 */
add_action( 'admin_notices', function() {
    // Only show on our admin page and when synced
    if ( ! isset( $_GET['mu_synced'] ) ) {
        return;
    }
    
    // Verify we're on the right page
    $screen = get_current_screen();
    if ( ! $screen || false === strpos( $screen->id, 'wpcode_snippets_sync' ) ) {
        return;
    }
    
    $result = get_transient( 'kic_mu_sync_result' );
    delete_transient( 'kic_mu_sync_result' );
    
    if ( ! $result ) {
        return;
    }
    
    $class = $result['success'] ? 'notice-success' : 'notice-error';
    ?>
    <div class="notice <?php echo esc_attr( $class ); ?> is-dismissible">
        <p><strong><?php esc_html_e( 'MU-Plugins Sync:', 'kic-wpcode-sync' ); ?></strong> <?php echo esc_html( $result['message'] ); ?></p>
        <?php if ( ! empty( $result['errors'] ) ) : ?>
            <p><?php esc_html_e( 'Failed:', 'kic-wpcode-sync' ); ?> <?php echo esc_html( implode( ', ', $result['errors'] ) ); ?></p>
        <?php endif; ?>
    </div>
    <?php
} );
/**
 * Handle WPCode sync form submission (from the control panel)
 * BEST PRACTICE: Capability check BEFORE nonce verification (per WPCS/Patchstack)
 */
add_action( 'admin_init', function() {
    // Check if WPCode sync form was submitted
    if ( ! isset( $_POST['kic_sync_wpcode_snippets'] ) ) {
        return;
    }
    
    // BEST PRACTICE: Capability check FIRST (per WPCS/Patchstack)
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 
            esc_html__( 'You do not have permission to sync WPCode snippets.', 'kic-wpcode-sync' ),
            esc_html__( 'Permission Denied', 'kic-wpcode-sync' ),
            array( 'response' => 403 )
        );
    }
    
    // THEN nonce verification - sanitize_text_field + wp_unslash per WPCS
    if ( ! isset( $_POST['kic_wpcode_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kic_wpcode_nonce'] ) ), 'kic_sync_wpcode_snippets_action' ) ) {
        wp_die( 
            esc_html__( 'Security check failed. Please try again.', 'kic-wpcode-sync' ),
            esc_html__( 'Security Error', 'kic-wpcode-sync' ),
            array( 'response' => 403 )
        );
    }
    
    // Check if the WPCode sync function exists (from AD_SYNC snippet)
    if ( ! function_exists( 'kic_sync_all_wpcode_snippets' ) ) {
        set_transient( 'kic_wpcode_sync_result', array(
            'success' => false,
            'message' => 'WPCode Sync Engine (AD_SYNC) not installed or inactive.',
        ), 30 );
        
        $redirect_url = wp_get_referer();
        if ( ! $redirect_url ) {
            $redirect_url = admin_url( 'edit.php?post_type=wpcode_snippets_sync' );
        }
        wp_safe_redirect( add_query_arg( 'wpcode_synced', '1', $redirect_url ) );
        exit;
    }
    
    // Perform WPCode sync using the function from AD_SYNC
    $result = kic_sync_all_wpcode_snippets();
    
    // Store result for display
    set_transient( 'kic_wpcode_sync_result', $result, 30 );
    
    // Redirect to avoid form resubmission - use wp_safe_redirect
    $redirect_url = wp_get_referer();
    if ( ! $redirect_url ) {
        $redirect_url = admin_url( 'edit.php?post_type=wpcode_snippets_sync' );
    }
    
    wp_safe_redirect( add_query_arg( 'wpcode_synced', '1', $redirect_url ) );
    exit;
} );
/**
 * Display WPCode sync result notice
 */
add_action( 'admin_notices', function() {
    // Only show when WPCode was synced
    if ( ! isset( $_GET['wpcode_synced'] ) ) {
        return;
    }
    
    // Verify we're on the right page
    $screen = get_current_screen();
    if ( ! $screen || false === strpos( $screen->id, 'wpcode_snippets_sync' ) ) {
        return;
    }
    
    $result = get_transient( 'kic_wpcode_sync_result' );
    delete_transient( 'kic_wpcode_sync_result' );
    
    if ( ! $result ) {
        return;
    }
    
    $class = $result['success'] ? 'notice-success' : 'notice-error';
    ?>
    <div class="notice <?php echo esc_attr( $class ); ?> is-dismissible">
        <p><strong><?php esc_html_e( 'WPCode Sync:', 'kic-wpcode-sync' ); ?></strong> <?php echo esc_html( $result['message'] ); ?></p>
        <?php if ( ! empty( $result['errors'] ) ) : ?>
            <p><?php esc_html_e( 'Failed:', 'kic-wpcode-sync' ); ?> <?php echo esc_html( implode( ', ', $result['errors'] ) ); ?></p>
        <?php endif; ?>
    </div>
    <?php
} );
/**
 * WP-CLI command for syncing MU-plugins
 * 
 * ## EXAMPLES
 * 
 *     # Sync all MU-plugins (root + all subfolders)
 *     wp kic sync-mu-plugins
 * 
 *     # Sync everything (WPCode + MU-plugins)
 *     wp kic sync-all
 */
if ( defined( 'WP_CLI' ) && WP_CLI ) {
    
    WP_CLI::add_command( 'kic sync-mu-plugins', function( $args, $assoc_args ) {
        WP_CLI::log( 'Starting MU-plugins sync (root + all subfolders)...' );
        
        $result = kic_sync_all_mu_plugins();
        
        if ( $result['success'] ) {
            WP_CLI::success( $result['message'] );
            if ( ! empty( $result['errors'] ) ) {
                WP_CLI::warning( 'Some files failed to sync: ' . implode( ', ', $result['errors'] ) );
            }
        } else {
            WP_CLI::error( $result['message'] );
        }
    } );
    
    // Combined command for syncing everything
    WP_CLI::add_command( 'kic sync-all', function( $args, $assoc_args ) {
        WP_CLI::log( 'Starting full sync (WPCode + MU-plugins + all subfolders)...' );
        
        // Sync WPCode snippets (if function exists from main sync engine)
        if ( function_exists( 'kic_sync_all_wpcode_snippets' ) ) {
            WP_CLI::log( 'Syncing WPCode snippets...' );
            $wpcode_result = kic_sync_all_wpcode_snippets();
            WP_CLI::log( $wpcode_result['message'] );
        } else {
            WP_CLI::warning( 'WPCode sync function not found. Skipping WPCode sync.' );
        }
        
        // Sync MU-plugins (now includes all subfolders)
        WP_CLI::log( 'Syncing MU-plugins + all subfolders...' );
        $mu_result = kic_sync_all_mu_plugins();
        WP_CLI::log( $mu_result['message'] );
        
        WP_CLI::success( 'Full sync complete!' );
    } );
}

Comments

Add a Comment