Plugin Directory

Changeset 3454875


Ignore:
Timestamp:
02/05/2026 06:10:26 PM (2 months ago)
Author:
samybaxy
Message:

Release v6.0.2 - WordPress 6.5+ Plugin Dependencies integration

Location:
samybaxy-hyperdrive
Files:
8 edited
1 copied

Legend:

Unmodified
Added
Removed
  • samybaxy-hyperdrive/tags/6.0.2/includes/class-dependency-detector.php

    r3451622 r3454875  
    33 * Dependency Detector for Samybaxy's Hyperdrive
    44 *
    5  * Intelligently detects plugin dependencies using:
    6  * - WordPress 6.5+ "Requires Plugins" header
    7  * - Code analysis for common dependency patterns
    8  * - Known plugin ecosystem relationships
    9  * - Heuristic-based implicit dependency detection
     5 * Intelligently detects plugin dependencies using a multi-layered approach:
     6 *
     7 * Layer 1: WordPress 6.5+ native WP_Plugin_Dependencies (most authoritative)
     8 * Layer 2: "Requires Plugins" header parsing (for WP < 6.5 or fallback)
     9 * Layer 3: Code analysis for common dependency patterns
     10 * Layer 4: Known plugin ecosystem relationships (hardcoded fallback)
     11 * Layer 5: Heuristic-based implicit dependency detection (naming patterns)
     12 *
     13 * Time Complexity:
     14 * - get_dependency_map(): O(1) amortized (cached in static + database)
     15 * - build_dependency_map(): O(n * m) where n = plugins, m = avg file size for analysis
     16 * - resolve_dependencies(): O(k + e) where k = plugins to load, e = total edges
     17 * - detect_circular_dependencies(): O(V + E) using DFS with coloring
     18 *
     19 * Space Complexity:
     20 * - Dependency map: O(n * d) where n = plugins, d = avg dependencies per plugin
     21 * - Circular detection: O(n) for visited/recursion stack sets
    1022 *
    1123 * @package SamybaxyHyperdrive
     24 * @since 6.0.0
     25 * @version 6.0.2
    1226 */
    1327
     
    2438
    2539    /**
     40     * Option name for storing circular dependencies
     41     */
     42    const CIRCULAR_DEPS_OPTION = 'shypdr_circular_dependencies';
     43
     44    /**
     45     * WordPress.org slug validation regex (matches WP core)
     46     * Only lowercase alphanumeric and hyphens, no leading/trailing hyphens
     47     */
     48    const SLUG_REGEX = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
     49
     50    /**
     51     * Static cache for dependency map (request lifetime)
     52     *
     53     * @var array|null
     54     */
     55    private static $cached_map = null;
     56
     57    /**
     58     * Static cache for circular dependencies
     59     *
     60     * @var array|null
     61     */
     62    private static $circular_deps_cache = null;
     63
     64    /**
     65     * Track if WP_Plugin_Dependencies is available
     66     *
     67     * @var bool|null
     68     */
     69    private static $wp_deps_available = null;
     70
     71    /**
    2672     * Known ecosystem patterns for fallback/validation
     73     * These are hardcoded relationships that may not be declared in headers
     74     *
     75     * @var array
    2776     */
    2877    private static $known_ecosystems = [
    29         'elementor' => ['elementor-pro', 'the-plus-addons-for-elementor-page-builder'],
     78        'elementor' => [
     79            'elementor-pro',
     80            'the-plus-addons-for-elementor-page-builder',
     81            'jetelements-for-elementor',
     82            'jetelementor',
     83        ],
    3084        'woocommerce' => [
    3185            'woocommerce-subscriptions',
    3286            'woocommerce-memberships',
     87            'woocommerce-product-bundles',
     88            'woocommerce-smart-coupons',
    3389            'jet-woo-builder',
     90            'jet-woo-product-gallery',
    3491            // Payment gateways - CRITICAL for checkout
    3592            'woocommerce-gateway-stripe',
     
    3794            'stripe',
    3895            'stripe-for-woocommerce',
     96            'stripe-payments',
    3997            'woocommerce-payments',
    4098            'woocommerce-paypal-payments',
     
    42100            'paystack',
    43101        ],
    44         'jet-engine' => ['jet-menu', 'jet-blocks', 'jet-elements', 'jet-tabs', 'jet-popup', 'jet-smart-filters'],
    45         'learnpress' => ['learnpress-prerequisites', 'learnpress-course-review'],
    46         'restrict-content-pro' => ['rcp-content-filter-utility'],
     102        'jet-engine' => [
     103            'jet-menu',
     104            'jet-blocks',
     105            'jet-elements',
     106            'jet-tabs',
     107            'jet-popup',
     108            'jet-smart-filters',
     109            'jet-blog',
     110            'jet-search',
     111            'jet-reviews',
     112            'jet-compare-wishlist',
     113            'jet-tricks',
     114            'jet-theme-core',
     115            'jetformbuilder',
     116            'jet-woo-builder',
     117            'jet-woo-product-gallery',
     118            'jet-appointments-booking',
     119            'jet-booking',
     120            'jet-engine-trim-callback',
     121            'jet-engine-attachment-link-callback',
     122            'jet-engine-custom-visibility-conditions',
     123            'jet-engine-dynamic-charts-module',
     124            'jet-engine-dynamic-tables-module',
     125        ],
     126        'learnpress' => [
     127            'learnpress-prerequisites',
     128            'learnpress-course-review',
     129            'learnpress-assignments',
     130            'learnpress-gradebook',
     131            'learnpress-certificates',
     132        ],
     133        'restrict-content-pro' => [
     134            'rcp-content-filter-utility',
     135            'rcp-csv-user-import',
     136        ],
     137        'fluentform' => [
     138            'fluentformpro',
     139            'fluent-forms-pro',
     140        ],
     141        'fluent-crm' => [
     142            'fluentcrm-pro',
     143        ],
     144        'uncanny-automator' => [
     145            'uncanny-automator-pro',
     146        ],
     147        'affiliatewp' => [
     148            'affiliatewp-allowed-products',
     149            'affiliatewp-recurring-referrals',
     150        ],
    47151    ];
    48152
    49153    /**
     154     * Class name to plugin slug mapping for code analysis
     155     *
     156     * @var array
     157     */
     158    private static $class_to_slug = [
     159        'WooCommerce'                                    => 'woocommerce',
     160        'Elementor\\Plugin'                              => 'elementor',
     161        'Elementor\\Core\\Base\\Module'                  => 'elementor',
     162        'ElementorPro\\Plugin'                           => 'elementor-pro',
     163        'Jet_Engine'                                     => 'jet-engine',
     164        'Jet_Engine_Base_Module'                         => 'jet-engine',
     165        'LearnPress'                                     => 'learnpress',
     166        'LP_Course'                                      => 'learnpress',
     167        'RCP_Requirements_Check'                         => 'restrict-content-pro',
     168        'Restrict_Content_Pro'                           => 'restrict-content-pro',
     169        'FluentForm\\Framework\\Foundation\\Application' => 'fluentform',
     170        'FluentCrm\\App\\App'                            => 'fluent-crm',
     171        'Jetstylemanager'                                => 'jetstylemanager',
     172        'AffiliateWP'                                    => 'affiliatewp',
     173        'Affiliate_WP'                                   => 'affiliatewp',
     174        'bbPress'                                        => 'bbpress',
     175        'BuddyPress'                                     => 'buddypress',
     176        'GravityForms'                                   => 'gravityforms',
     177        'GFAPI'                                          => 'gravityforms',
     178        'Tribe__Events__Main'                            => 'the-events-calendar',
     179    ];
     180
     181    /**
     182     * Constant to plugin slug mapping for code analysis
     183     *
     184     * @var array
     185     */
     186    private static $constant_to_slug = [
     187        'ELEMENTOR_VERSION'         => 'elementor',
     188        'ELEMENTOR_PRO_VERSION'     => 'elementor-pro',
     189        'WC_VERSION'                => 'woocommerce',
     190        'WOOCOMMERCE_VERSION'       => 'woocommerce',
     191        'JET_ENGINE_VERSION'        => 'jet-engine',
     192        'LEARNPRESS_VERSION'        => 'learnpress',
     193        'RCP_PLUGIN_VERSION'        => 'restrict-content-pro',
     194        'FLUENTFORM_VERSION'        => 'fluentform',
     195        'JETSTYLEMANAGER_VERSION'   => 'jetstylemanager',
     196        'JETSTYLEMANAGER_ACTIVE'    => 'jetstylemanager',
     197        'JETSTYLEMANAGER_PATH'      => 'jetstylemanager',
     198        'JETSTYLEMANAGER_SLUG'      => 'jetstylemanager',
     199        'JETSTYLEMANAGER_NAME'      => 'jetstylemanager',
     200        'JETSTYLEMANAGER_URL'       => 'jetstylemanager',
     201        'JETSTYLEMANAGER_FILE'      => 'jetstylemanager',
     202        'JETSTYLEMANAGER_PLUGIN_BASENAME' => 'jetstylemanager',
     203        'JETSTYLEMANAGER_PLUGIN_DIR'      => 'jetstylemanager',
     204        'JETSTYLEMANAGER_PLUGIN_URL'      => 'jetstylemanager',
     205        'JETSTYLEMANAGER_PLUGIN_FILE'     => 'jetstylemanager',
     206        'JETSTYLEMANAGER_PLUGIN_SLUG'     => 'jetstylemanager',
     207        'JETSTYLEMANAGER_PLUGIN_NAME'     => 'jetstylemanager',
     208        'JETSTYLEMANAGER_PLUGIN_VERSION'  => 'jetstylemanager',
     209        'JETSTYLEMANAGER_PLUGIN_PREFIX'   => 'jetstylemanager',
     210        'JETSTYLEMANAGER_PLUGIN_DIR_PATH' => 'jetstylemanager',
     211        'JETSTYLEMANAGER_PLUGIN_DIR_URL'  => 'jetstylemanager',
     212        'JETSTYLEMANAGER_PLUGIN_BASENAME_DIR' => 'jetstylemanager',
     213        'JETSTYLEMANAGER_PLUGIN_BASENAME_FILE' => 'jetstylemanager',
     214        'JET_SM_VERSION'            => 'jet-style-manager',
     215        'JETELEMENTS_VERSION'       => 'jet-elements',
     216        'JET_MENU_VERSION'          => 'jet-menu',
     217        'JET_BLOCKS_VERSION'        => 'jet-blocks',
     218        'JET_SMART_FILTERS_VERSION' => 'jet-smart-filters',
     219        'JET_POPUP_VERSION'         => 'jet-popup',
     220        'JETWOOBUILDER_VERSION'     => 'jet-woo-builder',
     221        'JETWOOGALLERY_VERSION'     => 'jet-woo-product-gallery',
     222        'JETFORMBUILDER_VERSION'    => 'jetformbuilder',
     223        'JETWOO_BUILDER_VERSION'    => 'jet-woo-builder',
     224        'JETWOO_PRODUCT_GALLERY_VERSION' => 'jet-woo-product-gallery',
     225        'JETWOO_PRODUCT_GALLERY'    => 'jet-woo-product-gallery',
     226        'JETWOO_BUILDER'            => 'jet-woo-builder',
     227        'JETWOO_BUILDER_URL'        => 'jet-woo-builder',
     228        'JETWOO_BUILDER_PATH'       => 'jet-woo-builder',
     229        'JETWOO_BUILDER_FILE'       => 'jet-woo-builder',
     230        'JETWOO_BUILDER_SLUG'       => 'jet-woo-builder',
     231        'JETWOO_BUILDER_NAME'       => 'jet-woo-builder',
     232        'JETWOO_BUILDER_PLUGIN_FILE' => 'jet-woo-builder',
     233        'JETWOO_BUILDER_PLUGIN_SLUG' => 'jet-woo-builder',
     234        'JETWOO_BUILDER_PLUGIN_NAME' => 'jet-woo-builder',
     235        'JETWOO_BUILDER_PLUGIN_VERSION' => 'jet-woo-builder',
     236        'JETWOO_BUILDER_PLUGIN_PREFIX' => 'jet-woo-builder',
     237        'JETWOO_BUILDER_PLUGIN_DIR_PATH' => 'jet-woo-builder',
     238        'JETWOO_BUILDER_PLUGIN_DIR_URL' => 'jet-woo-builder',
     239        'JETWOO_BUILDER_PLUGIN_BASENAME_DIR' => 'jet-woo-builder',
     240        'JETWOO_BUILDER_PLUGIN_BASENAME_FILE' => 'jet-woo-builder',
     241        'JETWOO_BUILDER_PLUGIN_BASENAME' => 'jet-woo-builder',
     242        'JETWOO_BUILDER_PLUGIN_DIR' => 'jet-woo-builder',
     243        'JETWOO_BUILDER_PLUGIN_URL' => 'jet-woo-builder',
     244        'JETWOO_PRODUCT_GALLERY_VERSION' => 'jet-woo-product-gallery',
     245        'JETWOO_PRODUCT_GALLERY_URL' => 'jet-woo-product-gallery',
     246        'JETWOO_PRODUCT_GALLERY_PATH' => 'jet-woo-product-gallery',
     247        'JETWOO_PRODUCT_GALLERY_FILE' => 'jet-woo-product-gallery',
     248        'JETWOO_PRODUCT_GALLERY_SLUG' => 'jet-woo-product-gallery',
     249        'JETWOO_PRODUCT_GALLERY_NAME' => 'jet-woo-product-gallery',
     250        'JETWOO_PRODUCT_GALLERY_PLUGIN_FILE' => 'jet-woo-product-gallery',
     251        'JETWOO_PRODUCT_GALLERY_PLUGIN_SLUG' => 'jet-woo-product-gallery',
     252        'JETWOO_PRODUCT_GALLERY_PLUGIN_NAME' => 'jet-woo-product-gallery',
     253        'AFFILIATEWP_VERSION'       => 'affiliatewp',
     254        'AFFILIATE_WP_VERSION'      => 'affiliatewp',
     255    ];
     256
     257    /**
     258     * Hook pattern to plugin slug mapping for code analysis
     259     *
     260     * @var array
     261     */
     262    private static $hook_to_slug = [
     263        'elementor/'                     => 'elementor',
     264        'elementor_pro/'                 => 'elementor-pro',
     265        'woocommerce_'                   => 'woocommerce',
     266        'woocommerce/'                   => 'woocommerce',
     267        'jet-engine/'                    => 'jet-engine',
     268        'jet_engine/'                    => 'jet-engine',
     269        'jet_engine_'                    => 'jet-engine',
     270        'learnpress_'                    => 'learnpress',
     271        'learnpress/'                    => 'learnpress',
     272        'learn_press_'                   => 'learnpress',
     273        'learn-press/'                   => 'learnpress',
     274        'rcp_'                           => 'restrict-content-pro',
     275        'fluentform_'                    => 'fluentform',
     276        'fluentform/'                    => 'fluentform',
     277        'fluentcrm_'                     => 'fluent-crm',
     278        'fluentcrm/'                     => 'fluent-crm',
     279        'jetstylemanager_'               => 'jetstylemanager',
     280        'jetstylemanager/'               => 'jetstylemanager',
     281        'jet_style_manager_'             => 'jet-style-manager',
     282        'jet-style-manager/'             => 'jet-style-manager',
     283        'jet-menu/'                      => 'jet-menu',
     284        'jet_menu/'                      => 'jet-menu',
     285        'jet_menu_'                      => 'jet-menu',
     286        'jet-blocks/'                    => 'jet-blocks',
     287        'jet_blocks/'                    => 'jet-blocks',
     288        'jet_blocks_'                    => 'jet-blocks',
     289        'jet-elements/'                  => 'jet-elements',
     290        'jet_elements/'                  => 'jet-elements',
     291        'jet_elements_'                  => 'jet-elements',
     292        'jet-smart-filters/'             => 'jet-smart-filters',
     293        'jet_smart_filters/'             => 'jet-smart-filters',
     294        'jet_smart_filters_'             => 'jet-smart-filters',
     295        'jet-popup/'                     => 'jet-popup',
     296        'jet_popup/'                     => 'jet-popup',
     297        'jet_popup_'                     => 'jet-popup',
     298        'jet-woo-builder/'               => 'jet-woo-builder',
     299        'jet_woo_builder/'               => 'jet-woo-builder',
     300        'jet_woo_builder_'               => 'jet-woo-builder',
     301        'jet-woo-product-gallery/'       => 'jet-woo-product-gallery',
     302        'jet_woo_product_gallery/'       => 'jet-woo-product-gallery',
     303        'jet_woo_product_gallery_'       => 'jet-woo-product-gallery',
     304        'jetformbuilder/'                => 'jetformbuilder',
     305        'jet_form_builder/'              => 'jetformbuilder',
     306        'jet_form_builder_'              => 'jetformbuilder',
     307        'affiliatewp_'                   => 'affiliatewp',
     308        'affiliate_wp_'                  => 'affiliatewp',
     309        'bbp_'                           => 'bbpress',
     310        'bbpress/'                       => 'bbpress',
     311        'bp_'                            => 'buddypress',
     312        'buddypress/'                    => 'buddypress',
     313        'gform_'                         => 'gravityforms',
     314        'gravityforms/'                  => 'gravityforms',
     315        'tribe_events_'                  => 'the-events-calendar',
     316    ];
     317
     318    /**
     319     * Check if WordPress 6.5+ WP_Plugin_Dependencies is available
     320     *
     321     * @return bool
     322     */
     323    public static function is_wp_plugin_dependencies_available() {
     324        if ( null !== self::$wp_deps_available ) {
     325            return self::$wp_deps_available;
     326        }
     327
     328        self::$wp_deps_available = class_exists( 'WP_Plugin_Dependencies' );
     329        return self::$wp_deps_available;
     330    }
     331
     332    /**
    50333     * Get the complete dependency map (with caching)
    51334     *
    52      * @return array Dependency map
     335     * Time Complexity: O(1) amortized (static + database cache)
     336     * Space Complexity: O(n * d) where n = plugins, d = avg deps
     337     *
     338     * @return array Dependency map with structure:
     339     *               [
     340     *                   'plugin-slug' => [
     341     *                       'depends_on' => ['parent-1', 'parent-2'],
     342     *                       'plugins_depending' => ['child-1', 'child-2'],
     343     *                       'source' => 'wp_core|header|code|pattern|ecosystem'
     344     *                   ]
     345     *               ]
    53346     */
    54347    public static function get_dependency_map() {
    55         static $cached_map = null;
    56 
    57         if ( null !== $cached_map ) {
    58             return $cached_map;
    59         }
    60 
    61         // Get from database
     348        // Level 1: Static cache (fastest)
     349        if ( null !== self::$cached_map ) {
     350            return self::$cached_map;
     351        }
     352
     353        // Level 2: Database cache
    62354        $map = get_option( self::DEPENDENCY_MAP_OPTION, false );
    63355
    64356        // If not found or empty, build it
    65         if ( false === $map || empty( $map ) ) {
     357        if ( false === $map || empty( $map ) || ! is_array( $map ) ) {
    66358            $map = self::build_dependency_map();
    67359            update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     
    71363        $map = apply_filters( 'shypdr_dependency_map', $map );
    72364
    73         $cached_map = $map;
     365        self::$cached_map = $map;
    74366        return $map;
    75367    }
     
    77369    /**
    78370     * Build dependency map by scanning all active plugins
     371     *
     372     * Uses a 5-layer detection strategy:
     373     * 1. WP_Plugin_Dependencies (WP 6.5+) - most authoritative
     374     * 2. "Requires Plugins" header parsing
     375     * 3. Code analysis (class_exists, defined, hooks)
     376     * 4. Pattern matching (naming conventions)
     377     * 5. Known ecosystem relationships
     378     *
     379     * Time Complexity: O(n * m) where n = plugins, m = avg file size
     380     * Space Complexity: O(n * d) for the map
    79381     *
    80382     * @return array Dependency map
     
    84386        $dependency_map = [];
    85387
     388        // Layer 1: Try WordPress 6.5+ native dependency system first
     389        if ( self::is_wp_plugin_dependencies_available() ) {
     390            $dependency_map = self::get_dependencies_from_wp_core( $active_plugins );
     391        }
     392
     393        // Layers 2-5: Scan each plugin for additional dependencies
    86394        foreach ( $active_plugins as $plugin_path ) {
    87395            $slug = self::get_plugin_slug( $plugin_path );
    88             $dependencies = self::detect_plugin_dependencies( $plugin_path );
     396            $dependencies = self::detect_plugin_dependencies( $plugin_path, $dependency_map );
    89397
    90398            if ( ! empty( $dependencies ) ) {
    91                 $dependency_map[ $slug ] = [
    92                     'depends_on' => $dependencies,
    93                     'plugins_depending' => [],
    94                 ];
     399                if ( ! isset( $dependency_map[ $slug ] ) ) {
     400                    $dependency_map[ $slug ] = [
     401                        'depends_on'        => [],
     402                        'plugins_depending' => [],
     403                        'source'            => 'heuristic',
     404                    ];
     405                }
     406
     407                // Merge dependencies, avoiding duplicates
     408                $dependency_map[ $slug ]['depends_on'] = array_unique(
     409                    array_merge( $dependency_map[ $slug ]['depends_on'], $dependencies )
     410                );
    95411            }
    96412        }
    97413
    98414        // Build reverse dependencies (who depends on this plugin)
     415        // Time: O(n * d) where d = avg dependencies per plugin
    99416        foreach ( $dependency_map as $plugin => $data ) {
    100417            foreach ( $data['depends_on'] as $required_plugin ) {
    101418                if ( ! isset( $dependency_map[ $required_plugin ] ) ) {
    102419                    $dependency_map[ $required_plugin ] = [
    103                         'depends_on' => [],
     420                        'depends_on'        => [],
    104421                        'plugins_depending' => [],
     422                        'source'            => 'inferred',
    105423                    ];
    106424                }
     
    111429        }
    112430
    113         // Merge with known ecosystems for validation
    114         $dependency_map = self::merge_known_ecosystems( $dependency_map );
     431        // Layer 5: Merge with known ecosystems for validation
     432        $dependency_map = self::merge_known_ecosystems( $dependency_map, $active_plugins );
     433
     434        // Detect and flag circular dependencies
     435        $circular = self::detect_circular_dependencies( $dependency_map );
     436        if ( ! empty( $circular ) ) {
     437            update_option( self::CIRCULAR_DEPS_OPTION, $circular, false );
     438            // Mark circular dependencies in the map
     439            foreach ( $circular as $pair ) {
     440                if ( isset( $dependency_map[ $pair[0] ] ) ) {
     441                    $dependency_map[ $pair[0] ]['has_circular'] = true;
     442                    $dependency_map[ $pair[0] ]['circular_with'] = $pair[1];
     443                }
     444            }
     445        }
    115446
    116447        return $dependency_map;
     
    118449
    119450    /**
    120      * Detect dependencies for a single plugin
     451     * Get dependencies from WordPress 6.5+ native WP_Plugin_Dependencies
     452     *
     453     * @param array $active_plugins List of active plugin paths
     454     * @return array Dependency map from WP core
     455     */
     456    private static function get_dependencies_from_wp_core( $active_plugins ) {
     457        $dependency_map = [];
     458
     459        if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) {
     460            return $dependency_map;
     461        }
     462
     463        // Initialize WP_Plugin_Dependencies if not already done
     464        WP_Plugin_Dependencies::initialize();
     465
     466        foreach ( $active_plugins as $plugin_path ) {
     467            $slug = self::get_plugin_slug( $plugin_path );
     468
     469            // Check if this plugin has dependencies via WP core
     470            if ( WP_Plugin_Dependencies::has_dependencies( $plugin_path ) ) {
     471                $deps = WP_Plugin_Dependencies::get_dependencies( $plugin_path );
     472
     473                if ( ! empty( $deps ) ) {
     474                    $dependency_map[ $slug ] = [
     475                        'depends_on'        => $deps,
     476                        'plugins_depending' => [],
     477                        'source'            => 'wp_core',
     478                    ];
     479                }
     480            }
     481
     482            // Check if this plugin has dependents via WP core
     483            if ( WP_Plugin_Dependencies::has_dependents( $plugin_path ) ) {
     484                if ( ! isset( $dependency_map[ $slug ] ) ) {
     485                    $dependency_map[ $slug ] = [
     486                        'depends_on'        => [],
     487                        'plugins_depending' => [],
     488                        'source'            => 'wp_core',
     489                    ];
     490                }
     491                // Dependents will be populated in the reverse pass
     492            }
     493        }
     494
     495        return $dependency_map;
     496    }
     497
     498    /**
     499     * Detect dependencies for a single plugin using multiple methods
    121500     *
    122501     * @param string $plugin_path Plugin file path
     502     * @param array  $existing_map Existing dependency map to avoid duplicate detection
    123503     * @return array Array of required plugin slugs
    124504     */
    125     private static function detect_plugin_dependencies( $plugin_path ) {
     505    private static function detect_plugin_dependencies( $plugin_path, $existing_map = [] ) {
    126506        $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path;
     507        $slug = self::get_plugin_slug( $plugin_path );
    127508        $dependencies = [];
    128509
     
    131512        }
    132513
    133         // Method 1: Check WordPress 6.5+ "Requires Plugins" header
     514        // Skip if already fully detected by WP core
     515        if ( isset( $existing_map[ $slug ] ) && 'wp_core' === $existing_map[ $slug ]['source'] ) {
     516            return []; // WP core already has authoritative data
     517        }
     518
     519        // Method 1: Check "Requires Plugins" header (for WP < 6.5 or as fallback)
    134520        $requires_plugins = self::get_requires_plugins_header( $plugin_file );
    135521        if ( ! empty( $requires_plugins ) ) {
     
    149535        }
    150536
    151         // Remove duplicates
     537        // Remove duplicates and self-references
    152538        $dependencies = array_unique( $dependencies );
    153 
    154         return $dependencies;
    155     }
    156 
    157     /**
    158      * Get "Requires Plugins" header from plugin file (WordPress 6.5+)
     539        $dependencies = array_filter( $dependencies, function( $dep ) use ( $slug ) {
     540            return $dep !== $slug;
     541        });
     542
     543        return array_values( $dependencies );
     544    }
     545
     546    /**
     547     * Get "Requires Plugins" header from plugin file
     548     *
     549     * Supports the WordPress 6.5+ header format and applies the
     550     * wp_plugin_dependencies_slug filter for premium/free plugin swapping.
    159551     *
    160552     * @param string $plugin_file Full path to plugin file
    161      * @return array Array of required plugin slugs
     553     * @return array Array of validated and sanitized plugin slugs
    162554     */
    163555    private static function get_requires_plugins_header( $plugin_file ) {
     556        // Use WordPress's get_plugin_data() which handles the header
     557        if ( ! function_exists( 'get_plugin_data' ) ) {
     558            require_once ABSPATH . 'wp-admin/includes/plugin.php';
     559        }
     560
    164561        $plugin_data = get_plugin_data( $plugin_file, false, false );
    165562
    166563        // Check for "Requires Plugins" header
    167         if ( ! empty( $plugin_data['RequiresPlugins'] ) ) {
    168             // Parse comma-separated list of plugin slugs
    169             $plugins = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) );
    170             return array_filter( $plugins );
    171         }
    172 
    173         return [];
     564        if ( empty( $plugin_data['RequiresPlugins'] ) ) {
     565            return [];
     566        }
     567
     568        // Parse and sanitize slugs (matching WP core's sanitize_dependency_slugs)
     569        return self::sanitize_dependency_slugs( $plugin_data['RequiresPlugins'] );
     570    }
     571
     572    /**
     573     * Sanitize dependency slugs (matching WordPress core's implementation)
     574     *
     575     * @param string $slugs Comma-separated string of plugin slugs
     576     * @return array Array of validated, sanitized slugs
     577     */
     578    public static function sanitize_dependency_slugs( $slugs ) {
     579        $sanitized_slugs = [];
     580        $slug_array = explode( ',', $slugs );
     581
     582        foreach ( $slug_array as $slug ) {
     583            $slug = trim( $slug );
     584
     585            /**
     586             * Filter a plugin dependency's slug before validation.
     587             *
     588             * Can be used to switch between free and premium plugin slugs.
     589             * This matches the WordPress core filter for compatibility.
     590             *
     591             * @since 6.0.2
     592             *
     593             * @param string $slug The plugin slug.
     594             */
     595            $slug = apply_filters( 'wp_plugin_dependencies_slug', $slug );
     596
     597            // Validate against WordPress.org slug format
     598            if ( self::is_valid_slug( $slug ) ) {
     599                $sanitized_slugs[] = $slug;
     600            }
     601        }
     602
     603        $sanitized_slugs = array_unique( $sanitized_slugs );
     604        sort( $sanitized_slugs );
     605
     606        return $sanitized_slugs;
     607    }
     608
     609    /**
     610     * Validate a plugin slug against WordPress.org format
     611     *
     612     * @param string $slug The slug to validate
     613     * @return bool True if valid
     614     */
     615    public static function is_valid_slug( $slug ) {
     616        if ( empty( $slug ) || ! is_string( $slug ) ) {
     617            return false;
     618        }
     619
     620        // Match WordPress.org slug format: lowercase alphanumeric and hyphens
     621        // No leading/trailing hyphens, no consecutive hyphens
     622        return (bool) preg_match( self::SLUG_REGEX, $slug );
    174623    }
    175624
    176625    /**
    177626     * Analyze plugin code for dependency patterns
     627     *
     628     * Scans for:
     629     * - class_exists() / function_exists() checks
     630     * - defined() constant checks
     631     * - do_action / apply_filters hook patterns
     632     *
     633     * Time Complexity: O(m) where m = file size (limited to 50KB)
    178634     *
    179635     * @param string $plugin_file Full path to plugin file
     
    183639        $dependencies = [];
    184640
    185         // Read first 50KB of plugin file for analysis
     641        // Read first 50KB of plugin file for analysis (performance limit)
     642        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    186643        $content = file_get_contents( $plugin_file, false, null, 0, 50000 );
    187644
     
    191648
    192649        // Pattern 1: Check for class_exists() or function_exists() checks
    193         // Example: if ( class_exists( 'WooCommerce' ) )
    194         $class_checks = [
    195             'WooCommerce' => 'woocommerce',
    196             'Elementor\\Plugin' => 'elementor',
    197             'Jet_Engine' => 'jet-engine',
    198             'LearnPress' => 'learnpress',
    199             'RCP_Requirements_Check' => 'restrict-content-pro',
    200             'FluentForm\\Framework\\Foundation\\Application' => 'fluentform',
    201         ];
    202 
    203         foreach ( $class_checks as $class_name => $plugin_slug ) {
    204             if ( false !== strpos( $content, $class_name ) ) {
     650        foreach ( self::$class_to_slug as $class_name => $plugin_slug ) {
     651            // Escape backslashes for class names with namespaces
     652            $escaped_class = str_replace( '\\', '\\\\', $class_name );
     653            if ( false !== strpos( $content, $class_name ) ||
     654                 preg_match( '/class_exists\s*\(\s*[\'"]' . preg_quote( $escaped_class, '/' ) . '[\'"]\s*\)/i', $content ) ) {
    205655                $dependencies[] = $plugin_slug;
    206656            }
     
    208658
    209659        // Pattern 2: Check for defined constants
    210         // Example: if ( defined( 'ELEMENTOR_VERSION' ) )
    211         $constant_checks = [
    212             'ELEMENTOR_VERSION' => 'elementor',
    213             'WC_VERSION' => 'woocommerce',
    214             'JET_ENGINE_VERSION' => 'jet-engine',
    215             'LEARNPRESS_VERSION' => 'learnpress',
    216         ];
    217 
    218         foreach ( $constant_checks as $constant => $plugin_slug ) {
     660        foreach ( self::$constant_to_slug as $constant => $plugin_slug ) {
    219661            if ( false !== strpos( $content, $constant ) ) {
    220662                $dependencies[] = $plugin_slug;
     
    222664        }
    223665
    224         // Pattern 3: Check for do_action/apply_filters with plugin-specific hooks
    225         // Example: do_action( 'elementor/widgets/widgets_registered' )
    226         $hook_patterns = [
    227             'elementor/' => 'elementor',
    228             'woocommerce_' => 'woocommerce',
    229             'jet-engine/' => 'jet-engine',
    230             'learnpress_' => 'learnpress',
    231         ];
    232 
    233         foreach ( $hook_patterns as $pattern => $plugin_slug ) {
     666        // Pattern 3: Check for plugin-specific hooks
     667        foreach ( self::$hook_to_slug as $pattern => $plugin_slug ) {
    234668            if ( false !== strpos( $content, $pattern ) ) {
    235669                $dependencies[] = $plugin_slug;
     
    250684        $dependencies = [];
    251685
    252         // Pattern: "parent-child" or "parent-addon"
     686        // Pattern rules: regex => parent plugin
    253687        $patterns = [
    254             // Jet plugins ecosystem
    255             '/^jet-(?!engine)/' => 'jet-engine', // jet-* (except jet-engine itself) depends on jet-engine
     688            // Jet plugins ecosystem (except jet-engine itself)
     689            '/^jet-(?!engine$)/' => 'jet-engine',
    256690
    257691            // Elementor ecosystem
    258692            '/^elementor-pro$/' => 'elementor',
    259             '/-for-elementor/' => 'elementor', // Addons for Elementor
     693            '/-for-elementor/' => 'elementor',
     694            '/-elementor-/' => 'elementor',
    260695
    261696            // WooCommerce ecosystem
    262             '/^woocommerce-(?!$)/' => 'woocommerce', // woocommerce-* depends on woocommerce
     697            '/^woocommerce-(?!$)/' => 'woocommerce',
     698            '/^woo-/' => 'woocommerce',
     699            '/-for-woocommerce/' => 'woocommerce',
     700            '/-woocommerce$/' => 'woocommerce',
    263701
    264702            // LearnPress ecosystem
    265             '/^learnpress-(?!$)/' => 'learnpress',
     703            '/^learnpress-/' => 'learnpress',
    266704
    267705            // Fluent ecosystem
    268706            '/^fluentformpro$/' => 'fluentform',
     707            '/^fluent-forms-pro$/' => 'fluentform',
    269708            '/^fluentcrm-pro$/' => 'fluent-crm',
     709
     710            // Restrict Content Pro ecosystem
     711            '/^rcp-/' => 'restrict-content-pro',
     712
     713            // AffiliateWP ecosystem
     714            '/^affiliatewp-/' => 'affiliatewp',
     715
     716            // Uncanny Automator
     717            '/^uncanny-automator-pro$/' => 'uncanny-automator',
     718
     719            // bbPress ecosystem
     720            '/^bbpress-/' => 'bbpress',
     721            '/-for-bbpress/' => 'bbpress',
     722
     723            // BuddyPress ecosystem
     724            '/^buddypress-/' => 'buddypress',
     725            '/-for-buddypress/' => 'buddypress',
     726
     727            // Gravity Forms ecosystem
     728            '/^gravityforms-/' => 'gravityforms',
     729            '/^gf-/' => 'gravityforms',
    270730        ];
    271731
    272732        foreach ( $patterns as $pattern => $parent_plugin ) {
    273             if ( preg_match( $pattern, $slug ) ) {
     733            if ( preg_match( $pattern, $slug ) && $slug !== $parent_plugin ) {
    274734                $dependencies[] = $parent_plugin;
    275735            }
    276736        }
    277737
    278         return $dependencies;
     738        return array_unique( $dependencies );
    279739    }
    280740
     
    282742     * Merge detected dependencies with known ecosystem relationships
    283743     *
     744     * This ensures we don't miss critical dependencies that may not be
     745     * declared in headers (especially for premium/non-WordPress.org plugins).
     746     *
    284747     * @param array $detected_map Detected dependency map
     748     * @param array $active_plugins List of active plugin paths
    285749     * @return array Merged dependency map
    286750     */
    287     private static function merge_known_ecosystems( $detected_map ) {
     751    private static function merge_known_ecosystems( $detected_map, $active_plugins ) {
     752        // Build active slugs set for O(1) lookup
     753        $active_slugs = [];
     754        foreach ( $active_plugins as $plugin_path ) {
     755            $active_slugs[ self::get_plugin_slug( $plugin_path ) ] = true;
     756        }
     757
    288758        foreach ( self::$known_ecosystems as $parent => $children ) {
    289759            foreach ( $children as $child ) {
    290                 // If child plugin is active, ensure dependency on parent is recorded
    291                 if ( isset( $detected_map[ $child ] ) ) {
    292                     if ( ! in_array( $parent, $detected_map[ $child ]['depends_on'], true ) ) {
    293                         $detected_map[ $child ]['depends_on'][] = $parent;
    294                     }
     760                // Only process if child plugin is actually active
     761                if ( ! isset( $active_slugs[ $child ] ) ) {
     762                    continue;
     763                }
     764
     765                // Ensure child has dependency on parent
     766                if ( ! isset( $detected_map[ $child ] ) ) {
     767                    $detected_map[ $child ] = [
     768                        'depends_on'        => [],
     769                        'plugins_depending' => [],
     770                        'source'            => 'ecosystem',
     771                    ];
     772                }
     773
     774                if ( ! in_array( $parent, $detected_map[ $child ]['depends_on'], true ) ) {
     775                    $detected_map[ $child ]['depends_on'][] = $parent;
    295776                }
    296777
     
    298779                if ( ! isset( $detected_map[ $parent ] ) ) {
    299780                    $detected_map[ $parent ] = [
    300                         'depends_on' => [],
     781                        'depends_on'        => [],
    301782                        'plugins_depending' => [],
     783                        'source'            => 'ecosystem',
    302784                    ];
    303785                }
     786
    304787                if ( ! in_array( $child, $detected_map[ $parent ]['plugins_depending'], true ) ) {
    305788                    $detected_map[ $parent ]['plugins_depending'][] = $child;
     
    312795
    313796    /**
     797     * Detect circular dependencies using DFS with three-color marking
     798     *
     799     * Uses the standard graph algorithm for cycle detection:
     800     * - WHITE (0): Not visited
     801     * - GRAY (1): Currently in recursion stack
     802     * - BLACK (2): Fully processed
     803     *
     804     * Time Complexity: O(V + E) where V = plugins, E = dependency edges
     805     * Space Complexity: O(V) for color array and recursion stack
     806     *
     807     * @param array $dependency_map The dependency map to check
     808     * @return array Array of circular dependency pairs [['a', 'b'], ['c', 'd']]
     809     */
     810    public static function detect_circular_dependencies( $dependency_map ) {
     811        $circular_pairs = [];
     812        $color = []; // 0 = white, 1 = gray, 2 = black
     813        $parent = []; // Track parent for path reconstruction
     814
     815        // Initialize all nodes as white
     816        foreach ( $dependency_map as $plugin => $data ) {
     817            $color[ $plugin ] = 0;
     818            // Also initialize nodes that are dependencies but may not be in map as keys
     819            foreach ( $data['depends_on'] as $dep ) {
     820                if ( ! isset( $color[ $dep ] ) ) {
     821                    $color[ $dep ] = 0;
     822                }
     823            }
     824        }
     825
     826        // DFS from each unvisited node
     827        foreach ( array_keys( $color ) as $plugin ) {
     828            if ( 0 === $color[ $plugin ] ) {
     829                self::dfs_detect_cycle( $plugin, $dependency_map, $color, $parent, $circular_pairs );
     830            }
     831        }
     832
     833        // Remove duplicate pairs (normalize order)
     834        $unique_pairs = [];
     835        foreach ( $circular_pairs as $pair ) {
     836            sort( $pair );
     837            $key = implode( '|', $pair );
     838            $unique_pairs[ $key ] = $pair;
     839        }
     840
     841        return array_values( $unique_pairs );
     842    }
     843
     844    /**
     845     * DFS helper for cycle detection
     846     *
     847     * @param string $node Current node
     848     * @param array  $dependency_map Dependency map
     849     * @param array  &$color Color array (modified)
     850     * @param array  &$parent Parent tracking array
     851     * @param array  &$circular_pairs Found circular pairs (modified)
     852     */
     853    private static function dfs_detect_cycle( $node, $dependency_map, &$color, &$parent, &$circular_pairs ) {
     854        // Mark as gray (in progress)
     855        $color[ $node ] = 1;
     856
     857        // Get dependencies for this node
     858        $deps = isset( $dependency_map[ $node ]['depends_on'] )
     859            ? $dependency_map[ $node ]['depends_on']
     860            : [];
     861
     862        foreach ( $deps as $dep ) {
     863            if ( ! isset( $color[ $dep ] ) ) {
     864                $color[ $dep ] = 0;
     865            }
     866
     867            if ( 0 === $color[ $dep ] ) {
     868                // White: not visited, recurse
     869                $parent[ $dep ] = $node;
     870                self::dfs_detect_cycle( $dep, $dependency_map, $color, $parent, $circular_pairs );
     871            } elseif ( 1 === $color[ $dep ] ) {
     872                // Gray: found a back edge = cycle
     873                $circular_pairs[] = [ $node, $dep ];
     874            }
     875            // Black: already fully processed, no action needed
     876        }
     877
     878        // Mark as black (fully processed)
     879        $color[ $node ] = 2;
     880    }
     881
     882    /**
     883     * Check if a plugin has circular dependencies
     884     *
     885     * @param string $plugin_slug Plugin slug to check
     886     * @return bool|array False if no circular deps, or array with the conflicting plugin
     887     */
     888    public static function has_circular_dependency( $plugin_slug ) {
     889        $map = self::get_dependency_map();
     890
     891        if ( isset( $map[ $plugin_slug ]['has_circular'] ) && $map[ $plugin_slug ]['has_circular'] ) {
     892            return [
     893                'has_circular'  => true,
     894                'circular_with' => $map[ $plugin_slug ]['circular_with'] ?? 'unknown',
     895            ];
     896        }
     897
     898        return false;
     899    }
     900
     901    /**
     902     * Get all circular dependencies
     903     *
     904     * @return array Array of circular dependency pairs
     905     */
     906    public static function get_circular_dependencies() {
     907        if ( null !== self::$circular_deps_cache ) {
     908            return self::$circular_deps_cache;
     909        }
     910
     911        self::$circular_deps_cache = get_option( self::CIRCULAR_DEPS_OPTION, [] );
     912        return self::$circular_deps_cache;
     913    }
     914
     915    /**
     916     * Resolve dependencies for a set of plugins (BFS approach)
     917     *
     918     * Given a set of required plugin slugs, returns the full set including
     919     * all transitive dependencies, properly handling:
     920     * - Direct dependencies (what the plugin requires)
     921     * - Reverse dependencies (what requires the plugin, if active)
     922     * - Circular dependency protection
     923     *
     924     * Time Complexity: O(k + e) where k = plugins to process, e = edges traversed
     925     * Space Complexity: O(k) for the queue and result set
     926     *
     927     * @param array $required_slugs Initial set of required plugin slugs
     928     * @param array $active_plugins Full list of active plugins (for reverse dep check)
     929     * @param bool  $include_reverse Whether to include reverse dependencies
     930     * @return array Complete set of plugins to load
     931     */
     932    public static function resolve_dependencies( $required_slugs, $active_plugins = [], $include_reverse = true ) {
     933        $dependency_map = self::get_dependency_map();
     934        $circular_deps = self::get_circular_dependencies();
     935
     936        // Build active slugs set for O(1) lookup
     937        $active_set = [];
     938        foreach ( $active_plugins as $plugin_path ) {
     939            $slug = self::get_plugin_slug( $plugin_path );
     940            $active_set[ $slug ] = true;
     941        }
     942
     943        // Build circular deps set for O(1) lookup
     944        $circular_set = [];
     945        foreach ( $circular_deps as $pair ) {
     946            $circular_set[ $pair[0] . '|' . $pair[1] ] = true;
     947            $circular_set[ $pair[1] . '|' . $pair[0] ] = true;
     948        }
     949
     950        $to_load = [];
     951        $queue = $required_slugs;
     952        $max_iterations = 1000; // Safety limit to prevent infinite loops
     953        $iterations = 0;
     954
     955        while ( ! empty( $queue ) && $iterations < $max_iterations ) {
     956            $iterations++;
     957            $slug = array_shift( $queue );
     958
     959            // Skip if already processed
     960            if ( isset( $to_load[ $slug ] ) ) {
     961                continue;
     962            }
     963
     964            $to_load[ $slug ] = true;
     965
     966            // Add direct dependencies
     967            if ( isset( $dependency_map[ $slug ]['depends_on'] ) ) {
     968                foreach ( $dependency_map[ $slug ]['depends_on'] as $dep ) {
     969                    // Check for circular dependency before adding
     970                    $pair_key = $slug . '|' . $dep;
     971                    if ( isset( $circular_set[ $pair_key ] ) ) {
     972                        // Skip circular dependency but log it
     973                        continue;
     974                    }
     975
     976                    if ( ! isset( $to_load[ $dep ] ) ) {
     977                        $queue[] = $dep;
     978                    }
     979                }
     980            }
     981
     982            // Add reverse dependencies (children that depend on this plugin)
     983            if ( $include_reverse && isset( $dependency_map[ $slug ]['plugins_depending'] ) ) {
     984                foreach ( $dependency_map[ $slug ]['plugins_depending'] as $rdep ) {
     985                    // Only add if the reverse dependency is active
     986                    if ( ! isset( $to_load[ $rdep ] ) && isset( $active_set[ $rdep ] ) ) {
     987                        // Check for circular dependency
     988                        $pair_key = $slug . '|' . $rdep;
     989                        if ( ! isset( $circular_set[ $pair_key ] ) ) {
     990                            $queue[] = $rdep;
     991                        }
     992                    }
     993                }
     994            }
     995        }
     996
     997        return array_keys( $to_load );
     998    }
     999
     1000    /**
    3141001     * Extract plugin slug from path
    3151002     *
    316      * @param string $plugin_path e.g., "elementor/elementor.php"
    317      * @return string e.g., "elementor"
    318      */
    319     private static function get_plugin_slug( $plugin_path ) {
    320         $parts = explode( '/', $plugin_path );
    321         return $parts[0] ?? '';
    322     }
    323 
    324     /**
    325      * Rebuild dependency map and clear cache
    326      *
    327      * @return int Number of dependencies detected
     1003     * @param string $plugin_path e.g., "elementor/elementor.php" or "hello.php"
     1004     * @return string e.g., "elementor" or "hello-dolly"
     1005     */
     1006    public static function get_plugin_slug( $plugin_path ) {
     1007        // Special case for hello.php (WordPress core oddity)
     1008        if ( 'hello.php' === $plugin_path ) {
     1009            return 'hello-dolly';
     1010        }
     1011
     1012        // Standard case: slug is directory name
     1013        if ( str_contains( $plugin_path, '/' ) ) {
     1014            return dirname( $plugin_path );
     1015        }
     1016
     1017        // Single-file plugin: slug is filename without .php
     1018        return str_replace( '.php', '', $plugin_path );
     1019    }
     1020
     1021    /**
     1022     * Rebuild dependency map and clear all caches
     1023     *
     1024     * @return array Statistics about the rebuild
    3281025     */
    3291026    public static function rebuild_dependency_map() {
     1027        // Clear all caches
     1028        self::$cached_map = null;
     1029        self::$circular_deps_cache = null;
    3301030        delete_option( self::DEPENDENCY_MAP_OPTION );
     1031        delete_option( self::CIRCULAR_DEPS_OPTION );
     1032
     1033        // Rebuild
    3311034        $map = self::build_dependency_map();
    3321035        update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
    3331036
    334         return count( $map );
     1037        // Get circular deps that were detected during build
     1038        $circular = get_option( self::CIRCULAR_DEPS_OPTION, [] );
     1039
     1040        return [
     1041            'total_plugins'                 => count( $map ),
     1042            'plugins_with_dependencies'     => count( array_filter( $map, function( $data ) {
     1043                return ! empty( $data['depends_on'] );
     1044            } ) ),
     1045            'total_dependency_relationships' => array_sum( array_map( function( $data ) {
     1046                return count( $data['depends_on'] );
     1047            }, $map ) ),
     1048            'circular_dependencies'         => count( $circular ),
     1049            'detection_sources'             => self::count_detection_sources( $map ),
     1050        ];
     1051    }
     1052
     1053    /**
     1054     * Count detection sources for statistics
     1055     *
     1056     * @param array $map Dependency map
     1057     * @return array Source counts
     1058     */
     1059    private static function count_detection_sources( $map ) {
     1060        $sources = [
     1061            'wp_core'   => 0,
     1062            'header'    => 0,
     1063            'code'      => 0,
     1064            'pattern'   => 0,
     1065            'ecosystem' => 0,
     1066            'heuristic' => 0,
     1067            'inferred'  => 0,
     1068        ];
     1069
     1070        foreach ( $map as $data ) {
     1071            $source = $data['source'] ?? 'unknown';
     1072            if ( isset( $sources[ $source ] ) ) {
     1073                $sources[ $source ]++;
     1074            }
     1075        }
     1076
     1077        return $sources;
    3351078    }
    3361079
     
    3391082     */
    3401083    public static function clear_cache() {
     1084        self::$cached_map = null;
     1085        self::$circular_deps_cache = null;
    3411086        delete_option( self::DEPENDENCY_MAP_OPTION );
     1087        delete_option( self::CIRCULAR_DEPS_OPTION );
    3421088    }
    3431089
     
    3491095    public static function get_stats() {
    3501096        $map = self::get_dependency_map();
     1097        $circular = self::get_circular_dependencies();
    3511098
    3521099        $total_plugins = count( $map );
    3531100        $plugins_with_deps = 0;
    3541101        $total_dependencies = 0;
     1102        $max_deps = 0;
     1103        $plugin_with_most_deps = '';
    3551104
    3561105        foreach ( $map as $plugin => $data ) {
    357             if ( ! empty( $data['depends_on'] ) ) {
     1106            $dep_count = count( $data['depends_on'] ?? [] );
     1107            if ( $dep_count > 0 ) {
    3581108                $plugins_with_deps++;
    359                 $total_dependencies += count( $data['depends_on'] );
     1109                $total_dependencies += $dep_count;
     1110                if ( $dep_count > $max_deps ) {
     1111                    $max_deps = $dep_count;
     1112                    $plugin_with_most_deps = $plugin;
     1113                }
    3601114            }
    3611115        }
    3621116
    3631117        return [
    364             'total_plugins' => $total_plugins,
    365             'plugins_with_dependencies' => $plugins_with_deps,
     1118            'total_plugins'                  => $total_plugins,
     1119            'plugins_with_dependencies'      => $plugins_with_deps,
    3661120            'total_dependency_relationships' => $total_dependencies,
    367             'detection_method' => 'heuristic_scan',
     1121            'circular_dependencies'          => count( $circular ),
     1122            'circular_pairs'                 => $circular,
     1123            'max_dependencies'               => $max_deps,
     1124            'plugin_with_most_dependencies'  => $plugin_with_most_deps,
     1125            'wp_plugin_dependencies_available' => self::is_wp_plugin_dependencies_available(),
     1126            'detection_sources'              => self::count_detection_sources( $map ),
    3681127        ];
    3691128    }
     
    3771136     */
    3781137    public static function add_custom_dependency( $child_plugin, $parent_plugin ) {
     1138        // Validate slugs
     1139        if ( ! self::is_valid_slug( $child_plugin ) || ! self::is_valid_slug( $parent_plugin ) ) {
     1140            return false;
     1141        }
     1142
     1143        // Prevent self-dependency
     1144        if ( $child_plugin === $parent_plugin ) {
     1145            return false;
     1146        }
     1147
    3791148        $map = get_option( self::DEPENDENCY_MAP_OPTION, [] );
    3801149
     1150        // Initialize child if needed
    3811151        if ( ! isset( $map[ $child_plugin ] ) ) {
    3821152            $map[ $child_plugin ] = [
    383                 'depends_on' => [],
     1153                'depends_on'        => [],
    3841154                'plugins_depending' => [],
     1155                'source'            => 'custom',
    3851156            ];
    3861157        }
    3871158
     1159        // Add dependency if not exists
    3881160        if ( ! in_array( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ) ) {
    3891161            $map[ $child_plugin ]['depends_on'][] = $parent_plugin;
    3901162        }
    3911163
    392         // Update reverse dependency
     1164        // Initialize parent if needed
    3931165        if ( ! isset( $map[ $parent_plugin ] ) ) {
    3941166            $map[ $parent_plugin ] = [
    395                 'depends_on' => [],
     1167                'depends_on'        => [],
    3961168                'plugins_depending' => [],
     1169                'source'            => 'custom',
    3971170            ];
    3981171        }
    3991172
     1173        // Add reverse dependency if not exists
    4001174        if ( ! in_array( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ) ) {
    4011175            $map[ $parent_plugin ]['plugins_depending'][] = $child_plugin;
    4021176        }
    4031177
     1178        // Check for circular dependency before saving
     1179        $test_circular = self::detect_circular_dependencies( $map );
     1180        foreach ( $test_circular as $pair ) {
     1181            if ( in_array( $child_plugin, $pair, true ) && in_array( $parent_plugin, $pair, true ) ) {
     1182                // This would create a circular dependency
     1183                return false;
     1184            }
     1185        }
     1186
     1187        // Clear cache and save
     1188        self::$cached_map = null;
    4041189        return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
    4051190    }
     
    4151200        $map = get_option( self::DEPENDENCY_MAP_OPTION, [] );
    4161201
     1202        $modified = false;
     1203
     1204        // Remove from child's depends_on
    4171205        if ( isset( $map[ $child_plugin ]['depends_on'] ) ) {
    4181206            $key = array_search( $parent_plugin, $map[ $child_plugin ]['depends_on'], true );
     
    4201208                unset( $map[ $child_plugin ]['depends_on'][ $key ] );
    4211209                $map[ $child_plugin ]['depends_on'] = array_values( $map[ $child_plugin ]['depends_on'] );
    422             }
    423         }
    424 
     1210                $modified = true;
     1211            }
     1212        }
     1213
     1214        // Remove from parent's plugins_depending
    4251215        if ( isset( $map[ $parent_plugin ]['plugins_depending'] ) ) {
    4261216            $key = array_search( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true );
     
    4281218                unset( $map[ $parent_plugin ]['plugins_depending'][ $key ] );
    4291219                $map[ $parent_plugin ]['plugins_depending'] = array_values( $map[ $parent_plugin ]['plugins_depending'] );
    430             }
    431         }
    432 
    433         return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     1220                $modified = true;
     1221            }
     1222        }
     1223
     1224        if ( $modified ) {
     1225            self::$cached_map = null;
     1226            return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     1227        }
     1228
     1229        return false;
     1230    }
     1231
     1232    /**
     1233     * Get dependencies for a specific plugin
     1234     *
     1235     * @param string $plugin_slug Plugin slug
     1236     * @return array Array of dependency slugs
     1237     */
     1238    public static function get_plugin_dependencies( $plugin_slug ) {
     1239        $map = self::get_dependency_map();
     1240
     1241        if ( isset( $map[ $plugin_slug ]['depends_on'] ) ) {
     1242            return $map[ $plugin_slug ]['depends_on'];
     1243        }
     1244
     1245        return [];
     1246    }
     1247
     1248    /**
     1249     * Get plugins that depend on a specific plugin
     1250     *
     1251     * @param string $plugin_slug Plugin slug
     1252     * @return array Array of dependent plugin slugs
     1253     */
     1254    public static function get_plugin_dependents( $plugin_slug ) {
     1255        $map = self::get_dependency_map();
     1256
     1257        if ( isset( $map[ $plugin_slug ]['plugins_depending'] ) ) {
     1258            return $map[ $plugin_slug ]['plugins_depending'];
     1259        }
     1260
     1261        return [];
     1262    }
     1263
     1264    /**
     1265     * Check if a plugin is a dependency of any other active plugin
     1266     *
     1267     * @param string $plugin_slug Plugin slug to check
     1268     * @return bool True if other plugins depend on this one
     1269     */
     1270    public static function is_required_by_others( $plugin_slug ) {
     1271        $dependents = self::get_plugin_dependents( $plugin_slug );
     1272        return ! empty( $dependents );
     1273    }
     1274
     1275    /**
     1276     * Get the detection source for a plugin's dependencies
     1277     *
     1278     * @param string $plugin_slug Plugin slug
     1279     * @return string Source type (wp_core, header, code, pattern, ecosystem, custom, unknown)
     1280     */
     1281    public static function get_detection_source( $plugin_slug ) {
     1282        $map = self::get_dependency_map();
     1283
     1284        if ( isset( $map[ $plugin_slug ]['source'] ) ) {
     1285            return $map[ $plugin_slug ]['source'];
     1286        }
     1287
     1288        return 'unknown';
    4341289    }
    4351290}
  • samybaxy-hyperdrive/tags/6.0.2/mu-loader/shypdr-mu-loader.php

    r3451625 r3454875  
    3434if (!defined('SHYPDR_MU_LOADER_ACTIVE')) {
    3535    define('SHYPDR_MU_LOADER_ACTIVE', true);
    36     define('SHYPDR_MU_LOADER_VERSION', '6.0.1'); // Optimized: Removed excessive reverse deps, streamlined checkout detection
     36    define('SHYPDR_MU_LOADER_VERSION', '6.0.2'); // Enhanced: WP 6.5+ Plugin Dependencies integration, circular dep protection
    3737}
    3838
     
    679679
    680680    /**
    681      * Resolve plugin dependencies (O(k) where k = required plugins)
     681     * Resolve plugin dependencies with circular dependency protection
     682     *
     683     * Time Complexity: O(k + e) where k = plugins to process, e = edges traversed
     684     * Space Complexity: O(k) for the queue and result set
     685     *
     686     * Uses database-stored dependency map when available (built by main plugin),
     687     * falls back to static hardcoded maps for performance when DB isn't ready.
    682688     */
    683689    private static function resolve_dependencies($required_slugs, $active_plugins) {
    684         // Dependency map
    685         static $dependencies = [
     690        // Try to get dependency map from database first (built by main plugin)
     691        // This includes WP 6.5+ Requires Plugins header data
     692        static $db_dependency_map = null;
     693        static $db_circular_deps = null;
     694
     695        if (null === $db_dependency_map) {
     696            global $wpdb;
     697            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     698            $result = $wpdb->get_var(
     699                $wpdb->prepare(
     700                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
     701                    'shypdr_dependency_map'
     702                )
     703            );
     704            if ($result) {
     705                $db_dependency_map = maybe_unserialize($result);
     706            }
     707            if (!is_array($db_dependency_map)) {
     708                $db_dependency_map = [];
     709            }
     710
     711            // Also get circular dependencies
     712            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     713            $circular_result = $wpdb->get_var(
     714                $wpdb->prepare(
     715                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
     716                    'shypdr_circular_dependencies'
     717                )
     718            );
     719            if ($circular_result) {
     720                $db_circular_deps = maybe_unserialize($circular_result);
     721            }
     722            if (!is_array($db_circular_deps)) {
     723                $db_circular_deps = [];
     724            }
     725        }
     726
     727        // Fallback static dependency map (used when DB map is empty)
     728        static $fallback_dependencies = [
    686729            'jet-menu' => ['jet-engine'],
    687730            'jet-blocks' => ['jet-engine'],
     
    733776        // Reverse dependencies (when parent is loaded, also load these children if active)
    734777        // NOTE: Payment gateways are NOT here - they're loaded via direct detection on checkout pages only
    735         static $reverse_deps = [
     778        static $fallback_reverse_deps = [
    736779            'jet-engine' => [
    737780                'jet-menu', 'jet-blocks', 'jet-theme-core', 'jet-elements', 'jet-tabs',
     
    748791        ];
    749792
     793        // Build circular deps set for O(1) lookup
     794        $circular_set = [];
     795        foreach ($db_circular_deps as $pair) {
     796            if (is_array($pair) && count($pair) >= 2) {
     797                $circular_set[$pair[0] . '|' . $pair[1]] = true;
     798                $circular_set[$pair[1] . '|' . $pair[0]] = true;
     799            }
     800        }
     801
    750802        // Build active plugins set for O(1) lookup
    751803        $active_set = [];
     
    757809        $to_load = [];
    758810        $queue = $required_slugs;
    759 
    760         while (!empty($queue)) {
     811        $max_iterations = 1000; // Safety limit to prevent infinite loops
     812        $iterations = 0;
     813
     814        while (!empty($queue) && $iterations < $max_iterations) {
     815            $iterations++;
    761816            $slug = array_shift($queue);
    762817
     
    767822            $to_load[$slug] = true;
    768823
    769             // Add dependencies
    770             if (isset($dependencies[$slug])) {
    771                 foreach ($dependencies[$slug] as $dep) {
    772                     if (!isset($to_load[$dep])) {
    773                         $queue[] = $dep;
    774                     }
     824            // Get dependencies from DB map first, then fallback
     825            $deps = [];
     826            $rdeps = [];
     827
     828            if (!empty($db_dependency_map[$slug])) {
     829                // Use database-stored dependencies (includes WP 6.5+ header data)
     830                $deps = $db_dependency_map[$slug]['depends_on'] ?? [];
     831                $rdeps = $db_dependency_map[$slug]['plugins_depending'] ?? [];
     832            } else {
     833                // Fallback to static map
     834                $deps = $fallback_dependencies[$slug] ?? [];
     835                $rdeps = $fallback_reverse_deps[$slug] ?? [];
     836            }
     837
     838            // Add direct dependencies
     839            foreach ($deps as $dep) {
     840                // Check for circular dependency before adding
     841                $pair_key = $slug . '|' . $dep;
     842                if (isset($circular_set[$pair_key])) {
     843                    continue; // Skip circular dependency
    775844                }
     845
     846                if (!isset($to_load[$dep])) {
     847                    $queue[] = $dep;
     848                }
    776849            }
    777850
    778851            // Add reverse dependencies (if active)
    779             if (isset($reverse_deps[$slug])) {
    780                 foreach ($reverse_deps[$slug] as $rdep) {
    781                     if (!isset($to_load[$rdep]) && isset($active_set[$rdep])) {
     852            foreach ($rdeps as $rdep) {
     853                if (!isset($to_load[$rdep]) && isset($active_set[$rdep])) {
     854                    // Check for circular dependency
     855                    $pair_key = $slug . '|' . $rdep;
     856                    if (!isset($circular_set[$pair_key])) {
    782857                        $queue[] = $rdep;
    783858                    }
  • samybaxy-hyperdrive/tags/6.0.2/readme.txt

    r3453775 r3454875  
    55Requires at least: 6.4
    66Tested up to: 6.9
    7 Stable tag: 6.0.1
     7Stable tag: 6.0.2
    88Requires PHP: 8.2
    99License: GPLv2 or later
     
    1515
    1616**Status:** Production Ready
    17 **Current Version:** 6.0.1
     17**Current Version:** 6.0.2
    1818
    1919Samybaxy's Hyperdrive makes WordPress sites **65-75% faster** by intelligently loading only the plugins needed for each page.
     
    8383* **Filter overhead:** < 2.5ms per request
    8484* **Server cost reduction:** 60-70% for same traffic
     85
     86= What's New in v6.0.2 =
     87
     88🔗 **WordPress 6.5+ Plugin Dependencies Integration**
     89
     90Full integration with WordPress core's plugin dependency system:
     91
     92* **WP_Plugin_Dependencies API** - Native support for WordPress 6.5+ dependency tracking
     93* **Requires Plugins Header** - Automatic parsing of the official plugin dependency header
     94* **Circular Dependency Detection** - Prevents infinite loops using DFS algorithm (O(V+E) complexity)
     95* **5-Layer Detection** - WP Core → Header → Code Analysis → Pattern Matching → Known Ecosystems
     96* **wp_plugin_dependencies_slug Filter** - Support for premium/free plugin slug swapping
     97
     98**Technical Improvements:**
     99* Database-backed dependency map for MU-loader
     100* Automatic map rebuild on plugin activation/deactivation
     101* Version upgrade detection with automatic updates
     102* Extended pattern detection for more plugins
    85103
    86104= What's New in v6.0.1 =
     
    287305
    288306== Changelog ==
     307
     308= 6.0.2 - February 5, 2026 =
     309🔗 WordPress 6.5+ Plugin Dependencies Integration
     310* ✨ New: Full integration with WordPress 6.5+ WP_Plugin_Dependencies API
     311* ✨ New: Native support for Requires Plugins header parsing
     312* ✨ New: Circular dependency detection using DFS with three-color marking
     313* ✨ New: Proper slug validation matching WordPress.org format
     314* ✨ New: Support for wp_plugin_dependencies_slug filter (premium/free plugin swapping)
     315* ✨ New: 5-layer dependency detection hierarchy
     316* 🔧 Improved: MU-loader now uses database-stored dependency map
     317* 🔧 Improved: Automatic dependency map rebuild on plugin changes
     318* 🔧 Improved: Version upgrade detection with automatic MU-loader updates
     319* 🛡️ Safety: Circular dependency protection prevents infinite loops
     320* 🛡️ Safety: Max iteration limit as additional protection
    289321
    290322= 6.0.1 - February 1, 2026 =
  • samybaxy-hyperdrive/tags/6.0.2/samybaxy-hyperdrive.php

    r3451625 r3454875  
    44 * Plugin URI: https://github.com/samybaxy/samybaxy-hyperdrive
    55 * Description: Revolutionary plugin filtering - Load only essential plugins per page. Requires MU-plugin loader for actual performance gains.
    6  * Version: 6.0.1
     6 * Version: 6.0.2
    77 * Author: samybaxy
    88 * Author URI: https://github.com/samybaxy
     
    2222
    2323// Core initialization constants
    24 define('SHYPDR_VERSION', '6.0.1');
     24define('SHYPDR_VERSION', '6.0.2');
    2525define('SHYPDR_DIR', plugin_dir_path(__FILE__));
    2626define('SHYPDR_URL', plugin_dir_url(__FILE__));
     
    5555    // and prevents old MU-loaders from interfering with activation
    5656    shypdr_install_mu_loader();
    57 }
     57
     58    // Rebuild dependency map on activation (includes WP 6.5+ header support)
     59    if (class_exists('SHYPDR_Dependency_Detector')) {
     60        SHYPDR_Dependency_Detector::rebuild_dependency_map();
     61    }
     62
     63    // Store current version for upgrade detection
     64    update_option('shypdr_version', SHYPDR_VERSION);
     65}
     66
     67/**
     68 * Check for plugin version upgrade and run migrations
     69 */
     70function shypdr_check_version_upgrade() {
     71    $stored_version = get_option('shypdr_version', '0');
     72
     73    if (version_compare($stored_version, SHYPDR_VERSION, '<')) {
     74        // Version upgrade detected - rebuild dependency map
     75        // This ensures WP 6.5+ Requires Plugins header data is picked up
     76        if (class_exists('SHYPDR_Dependency_Detector')) {
     77            SHYPDR_Dependency_Detector::rebuild_dependency_map();
     78        }
     79
     80        // Update MU-loader to latest version
     81        shypdr_install_mu_loader();
     82
     83        // Store new version
     84        update_option('shypdr_version', SHYPDR_VERSION);
     85    }
     86}
     87add_action('admin_init', 'shypdr_check_version_upgrade');
    5888
    5989/**
  • samybaxy-hyperdrive/trunk/includes/class-dependency-detector.php

    r3451622 r3454875  
    33 * Dependency Detector for Samybaxy's Hyperdrive
    44 *
    5  * Intelligently detects plugin dependencies using:
    6  * - WordPress 6.5+ "Requires Plugins" header
    7  * - Code analysis for common dependency patterns
    8  * - Known plugin ecosystem relationships
    9  * - Heuristic-based implicit dependency detection
     5 * Intelligently detects plugin dependencies using a multi-layered approach:
     6 *
     7 * Layer 1: WordPress 6.5+ native WP_Plugin_Dependencies (most authoritative)
     8 * Layer 2: "Requires Plugins" header parsing (for WP < 6.5 or fallback)
     9 * Layer 3: Code analysis for common dependency patterns
     10 * Layer 4: Known plugin ecosystem relationships (hardcoded fallback)
     11 * Layer 5: Heuristic-based implicit dependency detection (naming patterns)
     12 *
     13 * Time Complexity:
     14 * - get_dependency_map(): O(1) amortized (cached in static + database)
     15 * - build_dependency_map(): O(n * m) where n = plugins, m = avg file size for analysis
     16 * - resolve_dependencies(): O(k + e) where k = plugins to load, e = total edges
     17 * - detect_circular_dependencies(): O(V + E) using DFS with coloring
     18 *
     19 * Space Complexity:
     20 * - Dependency map: O(n * d) where n = plugins, d = avg dependencies per plugin
     21 * - Circular detection: O(n) for visited/recursion stack sets
    1022 *
    1123 * @package SamybaxyHyperdrive
     24 * @since 6.0.0
     25 * @version 6.0.2
    1226 */
    1327
     
    2438
    2539    /**
     40     * Option name for storing circular dependencies
     41     */
     42    const CIRCULAR_DEPS_OPTION = 'shypdr_circular_dependencies';
     43
     44    /**
     45     * WordPress.org slug validation regex (matches WP core)
     46     * Only lowercase alphanumeric and hyphens, no leading/trailing hyphens
     47     */
     48    const SLUG_REGEX = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
     49
     50    /**
     51     * Static cache for dependency map (request lifetime)
     52     *
     53     * @var array|null
     54     */
     55    private static $cached_map = null;
     56
     57    /**
     58     * Static cache for circular dependencies
     59     *
     60     * @var array|null
     61     */
     62    private static $circular_deps_cache = null;
     63
     64    /**
     65     * Track if WP_Plugin_Dependencies is available
     66     *
     67     * @var bool|null
     68     */
     69    private static $wp_deps_available = null;
     70
     71    /**
    2672     * Known ecosystem patterns for fallback/validation
     73     * These are hardcoded relationships that may not be declared in headers
     74     *
     75     * @var array
    2776     */
    2877    private static $known_ecosystems = [
    29         'elementor' => ['elementor-pro', 'the-plus-addons-for-elementor-page-builder'],
     78        'elementor' => [
     79            'elementor-pro',
     80            'the-plus-addons-for-elementor-page-builder',
     81            'jetelements-for-elementor',
     82            'jetelementor',
     83        ],
    3084        'woocommerce' => [
    3185            'woocommerce-subscriptions',
    3286            'woocommerce-memberships',
     87            'woocommerce-product-bundles',
     88            'woocommerce-smart-coupons',
    3389            'jet-woo-builder',
     90            'jet-woo-product-gallery',
    3491            // Payment gateways - CRITICAL for checkout
    3592            'woocommerce-gateway-stripe',
     
    3794            'stripe',
    3895            'stripe-for-woocommerce',
     96            'stripe-payments',
    3997            'woocommerce-payments',
    4098            'woocommerce-paypal-payments',
     
    42100            'paystack',
    43101        ],
    44         'jet-engine' => ['jet-menu', 'jet-blocks', 'jet-elements', 'jet-tabs', 'jet-popup', 'jet-smart-filters'],
    45         'learnpress' => ['learnpress-prerequisites', 'learnpress-course-review'],
    46         'restrict-content-pro' => ['rcp-content-filter-utility'],
     102        'jet-engine' => [
     103            'jet-menu',
     104            'jet-blocks',
     105            'jet-elements',
     106            'jet-tabs',
     107            'jet-popup',
     108            'jet-smart-filters',
     109            'jet-blog',
     110            'jet-search',
     111            'jet-reviews',
     112            'jet-compare-wishlist',
     113            'jet-tricks',
     114            'jet-theme-core',
     115            'jetformbuilder',
     116            'jet-woo-builder',
     117            'jet-woo-product-gallery',
     118            'jet-appointments-booking',
     119            'jet-booking',
     120            'jet-engine-trim-callback',
     121            'jet-engine-attachment-link-callback',
     122            'jet-engine-custom-visibility-conditions',
     123            'jet-engine-dynamic-charts-module',
     124            'jet-engine-dynamic-tables-module',
     125        ],
     126        'learnpress' => [
     127            'learnpress-prerequisites',
     128            'learnpress-course-review',
     129            'learnpress-assignments',
     130            'learnpress-gradebook',
     131            'learnpress-certificates',
     132        ],
     133        'restrict-content-pro' => [
     134            'rcp-content-filter-utility',
     135            'rcp-csv-user-import',
     136        ],
     137        'fluentform' => [
     138            'fluentformpro',
     139            'fluent-forms-pro',
     140        ],
     141        'fluent-crm' => [
     142            'fluentcrm-pro',
     143        ],
     144        'uncanny-automator' => [
     145            'uncanny-automator-pro',
     146        ],
     147        'affiliatewp' => [
     148            'affiliatewp-allowed-products',
     149            'affiliatewp-recurring-referrals',
     150        ],
    47151    ];
    48152
    49153    /**
     154     * Class name to plugin slug mapping for code analysis
     155     *
     156     * @var array
     157     */
     158    private static $class_to_slug = [
     159        'WooCommerce'                                    => 'woocommerce',
     160        'Elementor\\Plugin'                              => 'elementor',
     161        'Elementor\\Core\\Base\\Module'                  => 'elementor',
     162        'ElementorPro\\Plugin'                           => 'elementor-pro',
     163        'Jet_Engine'                                     => 'jet-engine',
     164        'Jet_Engine_Base_Module'                         => 'jet-engine',
     165        'LearnPress'                                     => 'learnpress',
     166        'LP_Course'                                      => 'learnpress',
     167        'RCP_Requirements_Check'                         => 'restrict-content-pro',
     168        'Restrict_Content_Pro'                           => 'restrict-content-pro',
     169        'FluentForm\\Framework\\Foundation\\Application' => 'fluentform',
     170        'FluentCrm\\App\\App'                            => 'fluent-crm',
     171        'Jetstylemanager'                                => 'jetstylemanager',
     172        'AffiliateWP'                                    => 'affiliatewp',
     173        'Affiliate_WP'                                   => 'affiliatewp',
     174        'bbPress'                                        => 'bbpress',
     175        'BuddyPress'                                     => 'buddypress',
     176        'GravityForms'                                   => 'gravityforms',
     177        'GFAPI'                                          => 'gravityforms',
     178        'Tribe__Events__Main'                            => 'the-events-calendar',
     179    ];
     180
     181    /**
     182     * Constant to plugin slug mapping for code analysis
     183     *
     184     * @var array
     185     */
     186    private static $constant_to_slug = [
     187        'ELEMENTOR_VERSION'         => 'elementor',
     188        'ELEMENTOR_PRO_VERSION'     => 'elementor-pro',
     189        'WC_VERSION'                => 'woocommerce',
     190        'WOOCOMMERCE_VERSION'       => 'woocommerce',
     191        'JET_ENGINE_VERSION'        => 'jet-engine',
     192        'LEARNPRESS_VERSION'        => 'learnpress',
     193        'RCP_PLUGIN_VERSION'        => 'restrict-content-pro',
     194        'FLUENTFORM_VERSION'        => 'fluentform',
     195        'JETSTYLEMANAGER_VERSION'   => 'jetstylemanager',
     196        'JETSTYLEMANAGER_ACTIVE'    => 'jetstylemanager',
     197        'JETSTYLEMANAGER_PATH'      => 'jetstylemanager',
     198        'JETSTYLEMANAGER_SLUG'      => 'jetstylemanager',
     199        'JETSTYLEMANAGER_NAME'      => 'jetstylemanager',
     200        'JETSTYLEMANAGER_URL'       => 'jetstylemanager',
     201        'JETSTYLEMANAGER_FILE'      => 'jetstylemanager',
     202        'JETSTYLEMANAGER_PLUGIN_BASENAME' => 'jetstylemanager',
     203        'JETSTYLEMANAGER_PLUGIN_DIR'      => 'jetstylemanager',
     204        'JETSTYLEMANAGER_PLUGIN_URL'      => 'jetstylemanager',
     205        'JETSTYLEMANAGER_PLUGIN_FILE'     => 'jetstylemanager',
     206        'JETSTYLEMANAGER_PLUGIN_SLUG'     => 'jetstylemanager',
     207        'JETSTYLEMANAGER_PLUGIN_NAME'     => 'jetstylemanager',
     208        'JETSTYLEMANAGER_PLUGIN_VERSION'  => 'jetstylemanager',
     209        'JETSTYLEMANAGER_PLUGIN_PREFIX'   => 'jetstylemanager',
     210        'JETSTYLEMANAGER_PLUGIN_DIR_PATH' => 'jetstylemanager',
     211        'JETSTYLEMANAGER_PLUGIN_DIR_URL'  => 'jetstylemanager',
     212        'JETSTYLEMANAGER_PLUGIN_BASENAME_DIR' => 'jetstylemanager',
     213        'JETSTYLEMANAGER_PLUGIN_BASENAME_FILE' => 'jetstylemanager',
     214        'JET_SM_VERSION'            => 'jet-style-manager',
     215        'JETELEMENTS_VERSION'       => 'jet-elements',
     216        'JET_MENU_VERSION'          => 'jet-menu',
     217        'JET_BLOCKS_VERSION'        => 'jet-blocks',
     218        'JET_SMART_FILTERS_VERSION' => 'jet-smart-filters',
     219        'JET_POPUP_VERSION'         => 'jet-popup',
     220        'JETWOOBUILDER_VERSION'     => 'jet-woo-builder',
     221        'JETWOOGALLERY_VERSION'     => 'jet-woo-product-gallery',
     222        'JETFORMBUILDER_VERSION'    => 'jetformbuilder',
     223        'JETWOO_BUILDER_VERSION'    => 'jet-woo-builder',
     224        'JETWOO_PRODUCT_GALLERY_VERSION' => 'jet-woo-product-gallery',
     225        'JETWOO_PRODUCT_GALLERY'    => 'jet-woo-product-gallery',
     226        'JETWOO_BUILDER'            => 'jet-woo-builder',
     227        'JETWOO_BUILDER_URL'        => 'jet-woo-builder',
     228        'JETWOO_BUILDER_PATH'       => 'jet-woo-builder',
     229        'JETWOO_BUILDER_FILE'       => 'jet-woo-builder',
     230        'JETWOO_BUILDER_SLUG'       => 'jet-woo-builder',
     231        'JETWOO_BUILDER_NAME'       => 'jet-woo-builder',
     232        'JETWOO_BUILDER_PLUGIN_FILE' => 'jet-woo-builder',
     233        'JETWOO_BUILDER_PLUGIN_SLUG' => 'jet-woo-builder',
     234        'JETWOO_BUILDER_PLUGIN_NAME' => 'jet-woo-builder',
     235        'JETWOO_BUILDER_PLUGIN_VERSION' => 'jet-woo-builder',
     236        'JETWOO_BUILDER_PLUGIN_PREFIX' => 'jet-woo-builder',
     237        'JETWOO_BUILDER_PLUGIN_DIR_PATH' => 'jet-woo-builder',
     238        'JETWOO_BUILDER_PLUGIN_DIR_URL' => 'jet-woo-builder',
     239        'JETWOO_BUILDER_PLUGIN_BASENAME_DIR' => 'jet-woo-builder',
     240        'JETWOO_BUILDER_PLUGIN_BASENAME_FILE' => 'jet-woo-builder',
     241        'JETWOO_BUILDER_PLUGIN_BASENAME' => 'jet-woo-builder',
     242        'JETWOO_BUILDER_PLUGIN_DIR' => 'jet-woo-builder',
     243        'JETWOO_BUILDER_PLUGIN_URL' => 'jet-woo-builder',
     244        'JETWOO_PRODUCT_GALLERY_VERSION' => 'jet-woo-product-gallery',
     245        'JETWOO_PRODUCT_GALLERY_URL' => 'jet-woo-product-gallery',
     246        'JETWOO_PRODUCT_GALLERY_PATH' => 'jet-woo-product-gallery',
     247        'JETWOO_PRODUCT_GALLERY_FILE' => 'jet-woo-product-gallery',
     248        'JETWOO_PRODUCT_GALLERY_SLUG' => 'jet-woo-product-gallery',
     249        'JETWOO_PRODUCT_GALLERY_NAME' => 'jet-woo-product-gallery',
     250        'JETWOO_PRODUCT_GALLERY_PLUGIN_FILE' => 'jet-woo-product-gallery',
     251        'JETWOO_PRODUCT_GALLERY_PLUGIN_SLUG' => 'jet-woo-product-gallery',
     252        'JETWOO_PRODUCT_GALLERY_PLUGIN_NAME' => 'jet-woo-product-gallery',
     253        'AFFILIATEWP_VERSION'       => 'affiliatewp',
     254        'AFFILIATE_WP_VERSION'      => 'affiliatewp',
     255    ];
     256
     257    /**
     258     * Hook pattern to plugin slug mapping for code analysis
     259     *
     260     * @var array
     261     */
     262    private static $hook_to_slug = [
     263        'elementor/'                     => 'elementor',
     264        'elementor_pro/'                 => 'elementor-pro',
     265        'woocommerce_'                   => 'woocommerce',
     266        'woocommerce/'                   => 'woocommerce',
     267        'jet-engine/'                    => 'jet-engine',
     268        'jet_engine/'                    => 'jet-engine',
     269        'jet_engine_'                    => 'jet-engine',
     270        'learnpress_'                    => 'learnpress',
     271        'learnpress/'                    => 'learnpress',
     272        'learn_press_'                   => 'learnpress',
     273        'learn-press/'                   => 'learnpress',
     274        'rcp_'                           => 'restrict-content-pro',
     275        'fluentform_'                    => 'fluentform',
     276        'fluentform/'                    => 'fluentform',
     277        'fluentcrm_'                     => 'fluent-crm',
     278        'fluentcrm/'                     => 'fluent-crm',
     279        'jetstylemanager_'               => 'jetstylemanager',
     280        'jetstylemanager/'               => 'jetstylemanager',
     281        'jet_style_manager_'             => 'jet-style-manager',
     282        'jet-style-manager/'             => 'jet-style-manager',
     283        'jet-menu/'                      => 'jet-menu',
     284        'jet_menu/'                      => 'jet-menu',
     285        'jet_menu_'                      => 'jet-menu',
     286        'jet-blocks/'                    => 'jet-blocks',
     287        'jet_blocks/'                    => 'jet-blocks',
     288        'jet_blocks_'                    => 'jet-blocks',
     289        'jet-elements/'                  => 'jet-elements',
     290        'jet_elements/'                  => 'jet-elements',
     291        'jet_elements_'                  => 'jet-elements',
     292        'jet-smart-filters/'             => 'jet-smart-filters',
     293        'jet_smart_filters/'             => 'jet-smart-filters',
     294        'jet_smart_filters_'             => 'jet-smart-filters',
     295        'jet-popup/'                     => 'jet-popup',
     296        'jet_popup/'                     => 'jet-popup',
     297        'jet_popup_'                     => 'jet-popup',
     298        'jet-woo-builder/'               => 'jet-woo-builder',
     299        'jet_woo_builder/'               => 'jet-woo-builder',
     300        'jet_woo_builder_'               => 'jet-woo-builder',
     301        'jet-woo-product-gallery/'       => 'jet-woo-product-gallery',
     302        'jet_woo_product_gallery/'       => 'jet-woo-product-gallery',
     303        'jet_woo_product_gallery_'       => 'jet-woo-product-gallery',
     304        'jetformbuilder/'                => 'jetformbuilder',
     305        'jet_form_builder/'              => 'jetformbuilder',
     306        'jet_form_builder_'              => 'jetformbuilder',
     307        'affiliatewp_'                   => 'affiliatewp',
     308        'affiliate_wp_'                  => 'affiliatewp',
     309        'bbp_'                           => 'bbpress',
     310        'bbpress/'                       => 'bbpress',
     311        'bp_'                            => 'buddypress',
     312        'buddypress/'                    => 'buddypress',
     313        'gform_'                         => 'gravityforms',
     314        'gravityforms/'                  => 'gravityforms',
     315        'tribe_events_'                  => 'the-events-calendar',
     316    ];
     317
     318    /**
     319     * Check if WordPress 6.5+ WP_Plugin_Dependencies is available
     320     *
     321     * @return bool
     322     */
     323    public static function is_wp_plugin_dependencies_available() {
     324        if ( null !== self::$wp_deps_available ) {
     325            return self::$wp_deps_available;
     326        }
     327
     328        self::$wp_deps_available = class_exists( 'WP_Plugin_Dependencies' );
     329        return self::$wp_deps_available;
     330    }
     331
     332    /**
    50333     * Get the complete dependency map (with caching)
    51334     *
    52      * @return array Dependency map
     335     * Time Complexity: O(1) amortized (static + database cache)
     336     * Space Complexity: O(n * d) where n = plugins, d = avg deps
     337     *
     338     * @return array Dependency map with structure:
     339     *               [
     340     *                   'plugin-slug' => [
     341     *                       'depends_on' => ['parent-1', 'parent-2'],
     342     *                       'plugins_depending' => ['child-1', 'child-2'],
     343     *                       'source' => 'wp_core|header|code|pattern|ecosystem'
     344     *                   ]
     345     *               ]
    53346     */
    54347    public static function get_dependency_map() {
    55         static $cached_map = null;
    56 
    57         if ( null !== $cached_map ) {
    58             return $cached_map;
    59         }
    60 
    61         // Get from database
     348        // Level 1: Static cache (fastest)
     349        if ( null !== self::$cached_map ) {
     350            return self::$cached_map;
     351        }
     352
     353        // Level 2: Database cache
    62354        $map = get_option( self::DEPENDENCY_MAP_OPTION, false );
    63355
    64356        // If not found or empty, build it
    65         if ( false === $map || empty( $map ) ) {
     357        if ( false === $map || empty( $map ) || ! is_array( $map ) ) {
    66358            $map = self::build_dependency_map();
    67359            update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     
    71363        $map = apply_filters( 'shypdr_dependency_map', $map );
    72364
    73         $cached_map = $map;
     365        self::$cached_map = $map;
    74366        return $map;
    75367    }
     
    77369    /**
    78370     * Build dependency map by scanning all active plugins
     371     *
     372     * Uses a 5-layer detection strategy:
     373     * 1. WP_Plugin_Dependencies (WP 6.5+) - most authoritative
     374     * 2. "Requires Plugins" header parsing
     375     * 3. Code analysis (class_exists, defined, hooks)
     376     * 4. Pattern matching (naming conventions)
     377     * 5. Known ecosystem relationships
     378     *
     379     * Time Complexity: O(n * m) where n = plugins, m = avg file size
     380     * Space Complexity: O(n * d) for the map
    79381     *
    80382     * @return array Dependency map
     
    84386        $dependency_map = [];
    85387
     388        // Layer 1: Try WordPress 6.5+ native dependency system first
     389        if ( self::is_wp_plugin_dependencies_available() ) {
     390            $dependency_map = self::get_dependencies_from_wp_core( $active_plugins );
     391        }
     392
     393        // Layers 2-5: Scan each plugin for additional dependencies
    86394        foreach ( $active_plugins as $plugin_path ) {
    87395            $slug = self::get_plugin_slug( $plugin_path );
    88             $dependencies = self::detect_plugin_dependencies( $plugin_path );
     396            $dependencies = self::detect_plugin_dependencies( $plugin_path, $dependency_map );
    89397
    90398            if ( ! empty( $dependencies ) ) {
    91                 $dependency_map[ $slug ] = [
    92                     'depends_on' => $dependencies,
    93                     'plugins_depending' => [],
    94                 ];
     399                if ( ! isset( $dependency_map[ $slug ] ) ) {
     400                    $dependency_map[ $slug ] = [
     401                        'depends_on'        => [],
     402                        'plugins_depending' => [],
     403                        'source'            => 'heuristic',
     404                    ];
     405                }
     406
     407                // Merge dependencies, avoiding duplicates
     408                $dependency_map[ $slug ]['depends_on'] = array_unique(
     409                    array_merge( $dependency_map[ $slug ]['depends_on'], $dependencies )
     410                );
    95411            }
    96412        }
    97413
    98414        // Build reverse dependencies (who depends on this plugin)
     415        // Time: O(n * d) where d = avg dependencies per plugin
    99416        foreach ( $dependency_map as $plugin => $data ) {
    100417            foreach ( $data['depends_on'] as $required_plugin ) {
    101418                if ( ! isset( $dependency_map[ $required_plugin ] ) ) {
    102419                    $dependency_map[ $required_plugin ] = [
    103                         'depends_on' => [],
     420                        'depends_on'        => [],
    104421                        'plugins_depending' => [],
     422                        'source'            => 'inferred',
    105423                    ];
    106424                }
     
    111429        }
    112430
    113         // Merge with known ecosystems for validation
    114         $dependency_map = self::merge_known_ecosystems( $dependency_map );
     431        // Layer 5: Merge with known ecosystems for validation
     432        $dependency_map = self::merge_known_ecosystems( $dependency_map, $active_plugins );
     433
     434        // Detect and flag circular dependencies
     435        $circular = self::detect_circular_dependencies( $dependency_map );
     436        if ( ! empty( $circular ) ) {
     437            update_option( self::CIRCULAR_DEPS_OPTION, $circular, false );
     438            // Mark circular dependencies in the map
     439            foreach ( $circular as $pair ) {
     440                if ( isset( $dependency_map[ $pair[0] ] ) ) {
     441                    $dependency_map[ $pair[0] ]['has_circular'] = true;
     442                    $dependency_map[ $pair[0] ]['circular_with'] = $pair[1];
     443                }
     444            }
     445        }
    115446
    116447        return $dependency_map;
     
    118449
    119450    /**
    120      * Detect dependencies for a single plugin
     451     * Get dependencies from WordPress 6.5+ native WP_Plugin_Dependencies
     452     *
     453     * @param array $active_plugins List of active plugin paths
     454     * @return array Dependency map from WP core
     455     */
     456    private static function get_dependencies_from_wp_core( $active_plugins ) {
     457        $dependency_map = [];
     458
     459        if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) {
     460            return $dependency_map;
     461        }
     462
     463        // Initialize WP_Plugin_Dependencies if not already done
     464        WP_Plugin_Dependencies::initialize();
     465
     466        foreach ( $active_plugins as $plugin_path ) {
     467            $slug = self::get_plugin_slug( $plugin_path );
     468
     469            // Check if this plugin has dependencies via WP core
     470            if ( WP_Plugin_Dependencies::has_dependencies( $plugin_path ) ) {
     471                $deps = WP_Plugin_Dependencies::get_dependencies( $plugin_path );
     472
     473                if ( ! empty( $deps ) ) {
     474                    $dependency_map[ $slug ] = [
     475                        'depends_on'        => $deps,
     476                        'plugins_depending' => [],
     477                        'source'            => 'wp_core',
     478                    ];
     479                }
     480            }
     481
     482            // Check if this plugin has dependents via WP core
     483            if ( WP_Plugin_Dependencies::has_dependents( $plugin_path ) ) {
     484                if ( ! isset( $dependency_map[ $slug ] ) ) {
     485                    $dependency_map[ $slug ] = [
     486                        'depends_on'        => [],
     487                        'plugins_depending' => [],
     488                        'source'            => 'wp_core',
     489                    ];
     490                }
     491                // Dependents will be populated in the reverse pass
     492            }
     493        }
     494
     495        return $dependency_map;
     496    }
     497
     498    /**
     499     * Detect dependencies for a single plugin using multiple methods
    121500     *
    122501     * @param string $plugin_path Plugin file path
     502     * @param array  $existing_map Existing dependency map to avoid duplicate detection
    123503     * @return array Array of required plugin slugs
    124504     */
    125     private static function detect_plugin_dependencies( $plugin_path ) {
     505    private static function detect_plugin_dependencies( $plugin_path, $existing_map = [] ) {
    126506        $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path;
     507        $slug = self::get_plugin_slug( $plugin_path );
    127508        $dependencies = [];
    128509
     
    131512        }
    132513
    133         // Method 1: Check WordPress 6.5+ "Requires Plugins" header
     514        // Skip if already fully detected by WP core
     515        if ( isset( $existing_map[ $slug ] ) && 'wp_core' === $existing_map[ $slug ]['source'] ) {
     516            return []; // WP core already has authoritative data
     517        }
     518
     519        // Method 1: Check "Requires Plugins" header (for WP < 6.5 or as fallback)
    134520        $requires_plugins = self::get_requires_plugins_header( $plugin_file );
    135521        if ( ! empty( $requires_plugins ) ) {
     
    149535        }
    150536
    151         // Remove duplicates
     537        // Remove duplicates and self-references
    152538        $dependencies = array_unique( $dependencies );
    153 
    154         return $dependencies;
    155     }
    156 
    157     /**
    158      * Get "Requires Plugins" header from plugin file (WordPress 6.5+)
     539        $dependencies = array_filter( $dependencies, function( $dep ) use ( $slug ) {
     540            return $dep !== $slug;
     541        });
     542
     543        return array_values( $dependencies );
     544    }
     545
     546    /**
     547     * Get "Requires Plugins" header from plugin file
     548     *
     549     * Supports the WordPress 6.5+ header format and applies the
     550     * wp_plugin_dependencies_slug filter for premium/free plugin swapping.
    159551     *
    160552     * @param string $plugin_file Full path to plugin file
    161      * @return array Array of required plugin slugs
     553     * @return array Array of validated and sanitized plugin slugs
    162554     */
    163555    private static function get_requires_plugins_header( $plugin_file ) {
     556        // Use WordPress's get_plugin_data() which handles the header
     557        if ( ! function_exists( 'get_plugin_data' ) ) {
     558            require_once ABSPATH . 'wp-admin/includes/plugin.php';
     559        }
     560
    164561        $plugin_data = get_plugin_data( $plugin_file, false, false );
    165562
    166563        // Check for "Requires Plugins" header
    167         if ( ! empty( $plugin_data['RequiresPlugins'] ) ) {
    168             // Parse comma-separated list of plugin slugs
    169             $plugins = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) );
    170             return array_filter( $plugins );
    171         }
    172 
    173         return [];
     564        if ( empty( $plugin_data['RequiresPlugins'] ) ) {
     565            return [];
     566        }
     567
     568        // Parse and sanitize slugs (matching WP core's sanitize_dependency_slugs)
     569        return self::sanitize_dependency_slugs( $plugin_data['RequiresPlugins'] );
     570    }
     571
     572    /**
     573     * Sanitize dependency slugs (matching WordPress core's implementation)
     574     *
     575     * @param string $slugs Comma-separated string of plugin slugs
     576     * @return array Array of validated, sanitized slugs
     577     */
     578    public static function sanitize_dependency_slugs( $slugs ) {
     579        $sanitized_slugs = [];
     580        $slug_array = explode( ',', $slugs );
     581
     582        foreach ( $slug_array as $slug ) {
     583            $slug = trim( $slug );
     584
     585            /**
     586             * Filter a plugin dependency's slug before validation.
     587             *
     588             * Can be used to switch between free and premium plugin slugs.
     589             * This matches the WordPress core filter for compatibility.
     590             *
     591             * @since 6.0.2
     592             *
     593             * @param string $slug The plugin slug.
     594             */
     595            $slug = apply_filters( 'wp_plugin_dependencies_slug', $slug );
     596
     597            // Validate against WordPress.org slug format
     598            if ( self::is_valid_slug( $slug ) ) {
     599                $sanitized_slugs[] = $slug;
     600            }
     601        }
     602
     603        $sanitized_slugs = array_unique( $sanitized_slugs );
     604        sort( $sanitized_slugs );
     605
     606        return $sanitized_slugs;
     607    }
     608
     609    /**
     610     * Validate a plugin slug against WordPress.org format
     611     *
     612     * @param string $slug The slug to validate
     613     * @return bool True if valid
     614     */
     615    public static function is_valid_slug( $slug ) {
     616        if ( empty( $slug ) || ! is_string( $slug ) ) {
     617            return false;
     618        }
     619
     620        // Match WordPress.org slug format: lowercase alphanumeric and hyphens
     621        // No leading/trailing hyphens, no consecutive hyphens
     622        return (bool) preg_match( self::SLUG_REGEX, $slug );
    174623    }
    175624
    176625    /**
    177626     * Analyze plugin code for dependency patterns
     627     *
     628     * Scans for:
     629     * - class_exists() / function_exists() checks
     630     * - defined() constant checks
     631     * - do_action / apply_filters hook patterns
     632     *
     633     * Time Complexity: O(m) where m = file size (limited to 50KB)
    178634     *
    179635     * @param string $plugin_file Full path to plugin file
     
    183639        $dependencies = [];
    184640
    185         // Read first 50KB of plugin file for analysis
     641        // Read first 50KB of plugin file for analysis (performance limit)
     642        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    186643        $content = file_get_contents( $plugin_file, false, null, 0, 50000 );
    187644
     
    191648
    192649        // Pattern 1: Check for class_exists() or function_exists() checks
    193         // Example: if ( class_exists( 'WooCommerce' ) )
    194         $class_checks = [
    195             'WooCommerce' => 'woocommerce',
    196             'Elementor\\Plugin' => 'elementor',
    197             'Jet_Engine' => 'jet-engine',
    198             'LearnPress' => 'learnpress',
    199             'RCP_Requirements_Check' => 'restrict-content-pro',
    200             'FluentForm\\Framework\\Foundation\\Application' => 'fluentform',
    201         ];
    202 
    203         foreach ( $class_checks as $class_name => $plugin_slug ) {
    204             if ( false !== strpos( $content, $class_name ) ) {
     650        foreach ( self::$class_to_slug as $class_name => $plugin_slug ) {
     651            // Escape backslashes for class names with namespaces
     652            $escaped_class = str_replace( '\\', '\\\\', $class_name );
     653            if ( false !== strpos( $content, $class_name ) ||
     654                 preg_match( '/class_exists\s*\(\s*[\'"]' . preg_quote( $escaped_class, '/' ) . '[\'"]\s*\)/i', $content ) ) {
    205655                $dependencies[] = $plugin_slug;
    206656            }
     
    208658
    209659        // Pattern 2: Check for defined constants
    210         // Example: if ( defined( 'ELEMENTOR_VERSION' ) )
    211         $constant_checks = [
    212             'ELEMENTOR_VERSION' => 'elementor',
    213             'WC_VERSION' => 'woocommerce',
    214             'JET_ENGINE_VERSION' => 'jet-engine',
    215             'LEARNPRESS_VERSION' => 'learnpress',
    216         ];
    217 
    218         foreach ( $constant_checks as $constant => $plugin_slug ) {
     660        foreach ( self::$constant_to_slug as $constant => $plugin_slug ) {
    219661            if ( false !== strpos( $content, $constant ) ) {
    220662                $dependencies[] = $plugin_slug;
     
    222664        }
    223665
    224         // Pattern 3: Check for do_action/apply_filters with plugin-specific hooks
    225         // Example: do_action( 'elementor/widgets/widgets_registered' )
    226         $hook_patterns = [
    227             'elementor/' => 'elementor',
    228             'woocommerce_' => 'woocommerce',
    229             'jet-engine/' => 'jet-engine',
    230             'learnpress_' => 'learnpress',
    231         ];
    232 
    233         foreach ( $hook_patterns as $pattern => $plugin_slug ) {
     666        // Pattern 3: Check for plugin-specific hooks
     667        foreach ( self::$hook_to_slug as $pattern => $plugin_slug ) {
    234668            if ( false !== strpos( $content, $pattern ) ) {
    235669                $dependencies[] = $plugin_slug;
     
    250684        $dependencies = [];
    251685
    252         // Pattern: "parent-child" or "parent-addon"
     686        // Pattern rules: regex => parent plugin
    253687        $patterns = [
    254             // Jet plugins ecosystem
    255             '/^jet-(?!engine)/' => 'jet-engine', // jet-* (except jet-engine itself) depends on jet-engine
     688            // Jet plugins ecosystem (except jet-engine itself)
     689            '/^jet-(?!engine$)/' => 'jet-engine',
    256690
    257691            // Elementor ecosystem
    258692            '/^elementor-pro$/' => 'elementor',
    259             '/-for-elementor/' => 'elementor', // Addons for Elementor
     693            '/-for-elementor/' => 'elementor',
     694            '/-elementor-/' => 'elementor',
    260695
    261696            // WooCommerce ecosystem
    262             '/^woocommerce-(?!$)/' => 'woocommerce', // woocommerce-* depends on woocommerce
     697            '/^woocommerce-(?!$)/' => 'woocommerce',
     698            '/^woo-/' => 'woocommerce',
     699            '/-for-woocommerce/' => 'woocommerce',
     700            '/-woocommerce$/' => 'woocommerce',
    263701
    264702            // LearnPress ecosystem
    265             '/^learnpress-(?!$)/' => 'learnpress',
     703            '/^learnpress-/' => 'learnpress',
    266704
    267705            // Fluent ecosystem
    268706            '/^fluentformpro$/' => 'fluentform',
     707            '/^fluent-forms-pro$/' => 'fluentform',
    269708            '/^fluentcrm-pro$/' => 'fluent-crm',
     709
     710            // Restrict Content Pro ecosystem
     711            '/^rcp-/' => 'restrict-content-pro',
     712
     713            // AffiliateWP ecosystem
     714            '/^affiliatewp-/' => 'affiliatewp',
     715
     716            // Uncanny Automator
     717            '/^uncanny-automator-pro$/' => 'uncanny-automator',
     718
     719            // bbPress ecosystem
     720            '/^bbpress-/' => 'bbpress',
     721            '/-for-bbpress/' => 'bbpress',
     722
     723            // BuddyPress ecosystem
     724            '/^buddypress-/' => 'buddypress',
     725            '/-for-buddypress/' => 'buddypress',
     726
     727            // Gravity Forms ecosystem
     728            '/^gravityforms-/' => 'gravityforms',
     729            '/^gf-/' => 'gravityforms',
    270730        ];
    271731
    272732        foreach ( $patterns as $pattern => $parent_plugin ) {
    273             if ( preg_match( $pattern, $slug ) ) {
     733            if ( preg_match( $pattern, $slug ) && $slug !== $parent_plugin ) {
    274734                $dependencies[] = $parent_plugin;
    275735            }
    276736        }
    277737
    278         return $dependencies;
     738        return array_unique( $dependencies );
    279739    }
    280740
     
    282742     * Merge detected dependencies with known ecosystem relationships
    283743     *
     744     * This ensures we don't miss critical dependencies that may not be
     745     * declared in headers (especially for premium/non-WordPress.org plugins).
     746     *
    284747     * @param array $detected_map Detected dependency map
     748     * @param array $active_plugins List of active plugin paths
    285749     * @return array Merged dependency map
    286750     */
    287     private static function merge_known_ecosystems( $detected_map ) {
     751    private static function merge_known_ecosystems( $detected_map, $active_plugins ) {
     752        // Build active slugs set for O(1) lookup
     753        $active_slugs = [];
     754        foreach ( $active_plugins as $plugin_path ) {
     755            $active_slugs[ self::get_plugin_slug( $plugin_path ) ] = true;
     756        }
     757
    288758        foreach ( self::$known_ecosystems as $parent => $children ) {
    289759            foreach ( $children as $child ) {
    290                 // If child plugin is active, ensure dependency on parent is recorded
    291                 if ( isset( $detected_map[ $child ] ) ) {
    292                     if ( ! in_array( $parent, $detected_map[ $child ]['depends_on'], true ) ) {
    293                         $detected_map[ $child ]['depends_on'][] = $parent;
    294                     }
     760                // Only process if child plugin is actually active
     761                if ( ! isset( $active_slugs[ $child ] ) ) {
     762                    continue;
     763                }
     764
     765                // Ensure child has dependency on parent
     766                if ( ! isset( $detected_map[ $child ] ) ) {
     767                    $detected_map[ $child ] = [
     768                        'depends_on'        => [],
     769                        'plugins_depending' => [],
     770                        'source'            => 'ecosystem',
     771                    ];
     772                }
     773
     774                if ( ! in_array( $parent, $detected_map[ $child ]['depends_on'], true ) ) {
     775                    $detected_map[ $child ]['depends_on'][] = $parent;
    295776                }
    296777
     
    298779                if ( ! isset( $detected_map[ $parent ] ) ) {
    299780                    $detected_map[ $parent ] = [
    300                         'depends_on' => [],
     781                        'depends_on'        => [],
    301782                        'plugins_depending' => [],
     783                        'source'            => 'ecosystem',
    302784                    ];
    303785                }
     786
    304787                if ( ! in_array( $child, $detected_map[ $parent ]['plugins_depending'], true ) ) {
    305788                    $detected_map[ $parent ]['plugins_depending'][] = $child;
     
    312795
    313796    /**
     797     * Detect circular dependencies using DFS with three-color marking
     798     *
     799     * Uses the standard graph algorithm for cycle detection:
     800     * - WHITE (0): Not visited
     801     * - GRAY (1): Currently in recursion stack
     802     * - BLACK (2): Fully processed
     803     *
     804     * Time Complexity: O(V + E) where V = plugins, E = dependency edges
     805     * Space Complexity: O(V) for color array and recursion stack
     806     *
     807     * @param array $dependency_map The dependency map to check
     808     * @return array Array of circular dependency pairs [['a', 'b'], ['c', 'd']]
     809     */
     810    public static function detect_circular_dependencies( $dependency_map ) {
     811        $circular_pairs = [];
     812        $color = []; // 0 = white, 1 = gray, 2 = black
     813        $parent = []; // Track parent for path reconstruction
     814
     815        // Initialize all nodes as white
     816        foreach ( $dependency_map as $plugin => $data ) {
     817            $color[ $plugin ] = 0;
     818            // Also initialize nodes that are dependencies but may not be in map as keys
     819            foreach ( $data['depends_on'] as $dep ) {
     820                if ( ! isset( $color[ $dep ] ) ) {
     821                    $color[ $dep ] = 0;
     822                }
     823            }
     824        }
     825
     826        // DFS from each unvisited node
     827        foreach ( array_keys( $color ) as $plugin ) {
     828            if ( 0 === $color[ $plugin ] ) {
     829                self::dfs_detect_cycle( $plugin, $dependency_map, $color, $parent, $circular_pairs );
     830            }
     831        }
     832
     833        // Remove duplicate pairs (normalize order)
     834        $unique_pairs = [];
     835        foreach ( $circular_pairs as $pair ) {
     836            sort( $pair );
     837            $key = implode( '|', $pair );
     838            $unique_pairs[ $key ] = $pair;
     839        }
     840
     841        return array_values( $unique_pairs );
     842    }
     843
     844    /**
     845     * DFS helper for cycle detection
     846     *
     847     * @param string $node Current node
     848     * @param array  $dependency_map Dependency map
     849     * @param array  &$color Color array (modified)
     850     * @param array  &$parent Parent tracking array
     851     * @param array  &$circular_pairs Found circular pairs (modified)
     852     */
     853    private static function dfs_detect_cycle( $node, $dependency_map, &$color, &$parent, &$circular_pairs ) {
     854        // Mark as gray (in progress)
     855        $color[ $node ] = 1;
     856
     857        // Get dependencies for this node
     858        $deps = isset( $dependency_map[ $node ]['depends_on'] )
     859            ? $dependency_map[ $node ]['depends_on']
     860            : [];
     861
     862        foreach ( $deps as $dep ) {
     863            if ( ! isset( $color[ $dep ] ) ) {
     864                $color[ $dep ] = 0;
     865            }
     866
     867            if ( 0 === $color[ $dep ] ) {
     868                // White: not visited, recurse
     869                $parent[ $dep ] = $node;
     870                self::dfs_detect_cycle( $dep, $dependency_map, $color, $parent, $circular_pairs );
     871            } elseif ( 1 === $color[ $dep ] ) {
     872                // Gray: found a back edge = cycle
     873                $circular_pairs[] = [ $node, $dep ];
     874            }
     875            // Black: already fully processed, no action needed
     876        }
     877
     878        // Mark as black (fully processed)
     879        $color[ $node ] = 2;
     880    }
     881
     882    /**
     883     * Check if a plugin has circular dependencies
     884     *
     885     * @param string $plugin_slug Plugin slug to check
     886     * @return bool|array False if no circular deps, or array with the conflicting plugin
     887     */
     888    public static function has_circular_dependency( $plugin_slug ) {
     889        $map = self::get_dependency_map();
     890
     891        if ( isset( $map[ $plugin_slug ]['has_circular'] ) && $map[ $plugin_slug ]['has_circular'] ) {
     892            return [
     893                'has_circular'  => true,
     894                'circular_with' => $map[ $plugin_slug ]['circular_with'] ?? 'unknown',
     895            ];
     896        }
     897
     898        return false;
     899    }
     900
     901    /**
     902     * Get all circular dependencies
     903     *
     904     * @return array Array of circular dependency pairs
     905     */
     906    public static function get_circular_dependencies() {
     907        if ( null !== self::$circular_deps_cache ) {
     908            return self::$circular_deps_cache;
     909        }
     910
     911        self::$circular_deps_cache = get_option( self::CIRCULAR_DEPS_OPTION, [] );
     912        return self::$circular_deps_cache;
     913    }
     914
     915    /**
     916     * Resolve dependencies for a set of plugins (BFS approach)
     917     *
     918     * Given a set of required plugin slugs, returns the full set including
     919     * all transitive dependencies, properly handling:
     920     * - Direct dependencies (what the plugin requires)
     921     * - Reverse dependencies (what requires the plugin, if active)
     922     * - Circular dependency protection
     923     *
     924     * Time Complexity: O(k + e) where k = plugins to process, e = edges traversed
     925     * Space Complexity: O(k) for the queue and result set
     926     *
     927     * @param array $required_slugs Initial set of required plugin slugs
     928     * @param array $active_plugins Full list of active plugins (for reverse dep check)
     929     * @param bool  $include_reverse Whether to include reverse dependencies
     930     * @return array Complete set of plugins to load
     931     */
     932    public static function resolve_dependencies( $required_slugs, $active_plugins = [], $include_reverse = true ) {
     933        $dependency_map = self::get_dependency_map();
     934        $circular_deps = self::get_circular_dependencies();
     935
     936        // Build active slugs set for O(1) lookup
     937        $active_set = [];
     938        foreach ( $active_plugins as $plugin_path ) {
     939            $slug = self::get_plugin_slug( $plugin_path );
     940            $active_set[ $slug ] = true;
     941        }
     942
     943        // Build circular deps set for O(1) lookup
     944        $circular_set = [];
     945        foreach ( $circular_deps as $pair ) {
     946            $circular_set[ $pair[0] . '|' . $pair[1] ] = true;
     947            $circular_set[ $pair[1] . '|' . $pair[0] ] = true;
     948        }
     949
     950        $to_load = [];
     951        $queue = $required_slugs;
     952        $max_iterations = 1000; // Safety limit to prevent infinite loops
     953        $iterations = 0;
     954
     955        while ( ! empty( $queue ) && $iterations < $max_iterations ) {
     956            $iterations++;
     957            $slug = array_shift( $queue );
     958
     959            // Skip if already processed
     960            if ( isset( $to_load[ $slug ] ) ) {
     961                continue;
     962            }
     963
     964            $to_load[ $slug ] = true;
     965
     966            // Add direct dependencies
     967            if ( isset( $dependency_map[ $slug ]['depends_on'] ) ) {
     968                foreach ( $dependency_map[ $slug ]['depends_on'] as $dep ) {
     969                    // Check for circular dependency before adding
     970                    $pair_key = $slug . '|' . $dep;
     971                    if ( isset( $circular_set[ $pair_key ] ) ) {
     972                        // Skip circular dependency but log it
     973                        continue;
     974                    }
     975
     976                    if ( ! isset( $to_load[ $dep ] ) ) {
     977                        $queue[] = $dep;
     978                    }
     979                }
     980            }
     981
     982            // Add reverse dependencies (children that depend on this plugin)
     983            if ( $include_reverse && isset( $dependency_map[ $slug ]['plugins_depending'] ) ) {
     984                foreach ( $dependency_map[ $slug ]['plugins_depending'] as $rdep ) {
     985                    // Only add if the reverse dependency is active
     986                    if ( ! isset( $to_load[ $rdep ] ) && isset( $active_set[ $rdep ] ) ) {
     987                        // Check for circular dependency
     988                        $pair_key = $slug . '|' . $rdep;
     989                        if ( ! isset( $circular_set[ $pair_key ] ) ) {
     990                            $queue[] = $rdep;
     991                        }
     992                    }
     993                }
     994            }
     995        }
     996
     997        return array_keys( $to_load );
     998    }
     999
     1000    /**
    3141001     * Extract plugin slug from path
    3151002     *
    316      * @param string $plugin_path e.g., "elementor/elementor.php"
    317      * @return string e.g., "elementor"
    318      */
    319     private static function get_plugin_slug( $plugin_path ) {
    320         $parts = explode( '/', $plugin_path );
    321         return $parts[0] ?? '';
    322     }
    323 
    324     /**
    325      * Rebuild dependency map and clear cache
    326      *
    327      * @return int Number of dependencies detected
     1003     * @param string $plugin_path e.g., "elementor/elementor.php" or "hello.php"
     1004     * @return string e.g., "elementor" or "hello-dolly"
     1005     */
     1006    public static function get_plugin_slug( $plugin_path ) {
     1007        // Special case for hello.php (WordPress core oddity)
     1008        if ( 'hello.php' === $plugin_path ) {
     1009            return 'hello-dolly';
     1010        }
     1011
     1012        // Standard case: slug is directory name
     1013        if ( str_contains( $plugin_path, '/' ) ) {
     1014            return dirname( $plugin_path );
     1015        }
     1016
     1017        // Single-file plugin: slug is filename without .php
     1018        return str_replace( '.php', '', $plugin_path );
     1019    }
     1020
     1021    /**
     1022     * Rebuild dependency map and clear all caches
     1023     *
     1024     * @return array Statistics about the rebuild
    3281025     */
    3291026    public static function rebuild_dependency_map() {
     1027        // Clear all caches
     1028        self::$cached_map = null;
     1029        self::$circular_deps_cache = null;
    3301030        delete_option( self::DEPENDENCY_MAP_OPTION );
     1031        delete_option( self::CIRCULAR_DEPS_OPTION );
     1032
     1033        // Rebuild
    3311034        $map = self::build_dependency_map();
    3321035        update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
    3331036
    334         return count( $map );
     1037        // Get circular deps that were detected during build
     1038        $circular = get_option( self::CIRCULAR_DEPS_OPTION, [] );
     1039
     1040        return [
     1041            'total_plugins'                 => count( $map ),
     1042            'plugins_with_dependencies'     => count( array_filter( $map, function( $data ) {
     1043                return ! empty( $data['depends_on'] );
     1044            } ) ),
     1045            'total_dependency_relationships' => array_sum( array_map( function( $data ) {
     1046                return count( $data['depends_on'] );
     1047            }, $map ) ),
     1048            'circular_dependencies'         => count( $circular ),
     1049            'detection_sources'             => self::count_detection_sources( $map ),
     1050        ];
     1051    }
     1052
     1053    /**
     1054     * Count detection sources for statistics
     1055     *
     1056     * @param array $map Dependency map
     1057     * @return array Source counts
     1058     */
     1059    private static function count_detection_sources( $map ) {
     1060        $sources = [
     1061            'wp_core'   => 0,
     1062            'header'    => 0,
     1063            'code'      => 0,
     1064            'pattern'   => 0,
     1065            'ecosystem' => 0,
     1066            'heuristic' => 0,
     1067            'inferred'  => 0,
     1068        ];
     1069
     1070        foreach ( $map as $data ) {
     1071            $source = $data['source'] ?? 'unknown';
     1072            if ( isset( $sources[ $source ] ) ) {
     1073                $sources[ $source ]++;
     1074            }
     1075        }
     1076
     1077        return $sources;
    3351078    }
    3361079
     
    3391082     */
    3401083    public static function clear_cache() {
     1084        self::$cached_map = null;
     1085        self::$circular_deps_cache = null;
    3411086        delete_option( self::DEPENDENCY_MAP_OPTION );
     1087        delete_option( self::CIRCULAR_DEPS_OPTION );
    3421088    }
    3431089
     
    3491095    public static function get_stats() {
    3501096        $map = self::get_dependency_map();
     1097        $circular = self::get_circular_dependencies();
    3511098
    3521099        $total_plugins = count( $map );
    3531100        $plugins_with_deps = 0;
    3541101        $total_dependencies = 0;
     1102        $max_deps = 0;
     1103        $plugin_with_most_deps = '';
    3551104
    3561105        foreach ( $map as $plugin => $data ) {
    357             if ( ! empty( $data['depends_on'] ) ) {
     1106            $dep_count = count( $data['depends_on'] ?? [] );
     1107            if ( $dep_count > 0 ) {
    3581108                $plugins_with_deps++;
    359                 $total_dependencies += count( $data['depends_on'] );
     1109                $total_dependencies += $dep_count;
     1110                if ( $dep_count > $max_deps ) {
     1111                    $max_deps = $dep_count;
     1112                    $plugin_with_most_deps = $plugin;
     1113                }
    3601114            }
    3611115        }
    3621116
    3631117        return [
    364             'total_plugins' => $total_plugins,
    365             'plugins_with_dependencies' => $plugins_with_deps,
     1118            'total_plugins'                  => $total_plugins,
     1119            'plugins_with_dependencies'      => $plugins_with_deps,
    3661120            'total_dependency_relationships' => $total_dependencies,
    367             'detection_method' => 'heuristic_scan',
     1121            'circular_dependencies'          => count( $circular ),
     1122            'circular_pairs'                 => $circular,
     1123            'max_dependencies'               => $max_deps,
     1124            'plugin_with_most_dependencies'  => $plugin_with_most_deps,
     1125            'wp_plugin_dependencies_available' => self::is_wp_plugin_dependencies_available(),
     1126            'detection_sources'              => self::count_detection_sources( $map ),
    3681127        ];
    3691128    }
     
    3771136     */
    3781137    public static function add_custom_dependency( $child_plugin, $parent_plugin ) {
     1138        // Validate slugs
     1139        if ( ! self::is_valid_slug( $child_plugin ) || ! self::is_valid_slug( $parent_plugin ) ) {
     1140            return false;
     1141        }
     1142
     1143        // Prevent self-dependency
     1144        if ( $child_plugin === $parent_plugin ) {
     1145            return false;
     1146        }
     1147
    3791148        $map = get_option( self::DEPENDENCY_MAP_OPTION, [] );
    3801149
     1150        // Initialize child if needed
    3811151        if ( ! isset( $map[ $child_plugin ] ) ) {
    3821152            $map[ $child_plugin ] = [
    383                 'depends_on' => [],
     1153                'depends_on'        => [],
    3841154                'plugins_depending' => [],
     1155                'source'            => 'custom',
    3851156            ];
    3861157        }
    3871158
     1159        // Add dependency if not exists
    3881160        if ( ! in_array( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ) ) {
    3891161            $map[ $child_plugin ]['depends_on'][] = $parent_plugin;
    3901162        }
    3911163
    392         // Update reverse dependency
     1164        // Initialize parent if needed
    3931165        if ( ! isset( $map[ $parent_plugin ] ) ) {
    3941166            $map[ $parent_plugin ] = [
    395                 'depends_on' => [],
     1167                'depends_on'        => [],
    3961168                'plugins_depending' => [],
     1169                'source'            => 'custom',
    3971170            ];
    3981171        }
    3991172
     1173        // Add reverse dependency if not exists
    4001174        if ( ! in_array( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ) ) {
    4011175            $map[ $parent_plugin ]['plugins_depending'][] = $child_plugin;
    4021176        }
    4031177
     1178        // Check for circular dependency before saving
     1179        $test_circular = self::detect_circular_dependencies( $map );
     1180        foreach ( $test_circular as $pair ) {
     1181            if ( in_array( $child_plugin, $pair, true ) && in_array( $parent_plugin, $pair, true ) ) {
     1182                // This would create a circular dependency
     1183                return false;
     1184            }
     1185        }
     1186
     1187        // Clear cache and save
     1188        self::$cached_map = null;
    4041189        return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
    4051190    }
     
    4151200        $map = get_option( self::DEPENDENCY_MAP_OPTION, [] );
    4161201
     1202        $modified = false;
     1203
     1204        // Remove from child's depends_on
    4171205        if ( isset( $map[ $child_plugin ]['depends_on'] ) ) {
    4181206            $key = array_search( $parent_plugin, $map[ $child_plugin ]['depends_on'], true );
     
    4201208                unset( $map[ $child_plugin ]['depends_on'][ $key ] );
    4211209                $map[ $child_plugin ]['depends_on'] = array_values( $map[ $child_plugin ]['depends_on'] );
    422             }
    423         }
    424 
     1210                $modified = true;
     1211            }
     1212        }
     1213
     1214        // Remove from parent's plugins_depending
    4251215        if ( isset( $map[ $parent_plugin ]['plugins_depending'] ) ) {
    4261216            $key = array_search( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true );
     
    4281218                unset( $map[ $parent_plugin ]['plugins_depending'][ $key ] );
    4291219                $map[ $parent_plugin ]['plugins_depending'] = array_values( $map[ $parent_plugin ]['plugins_depending'] );
    430             }
    431         }
    432 
    433         return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     1220                $modified = true;
     1221            }
     1222        }
     1223
     1224        if ( $modified ) {
     1225            self::$cached_map = null;
     1226            return update_option( self::DEPENDENCY_MAP_OPTION, $map, false );
     1227        }
     1228
     1229        return false;
     1230    }
     1231
     1232    /**
     1233     * Get dependencies for a specific plugin
     1234     *
     1235     * @param string $plugin_slug Plugin slug
     1236     * @return array Array of dependency slugs
     1237     */
     1238    public static function get_plugin_dependencies( $plugin_slug ) {
     1239        $map = self::get_dependency_map();
     1240
     1241        if ( isset( $map[ $plugin_slug ]['depends_on'] ) ) {
     1242            return $map[ $plugin_slug ]['depends_on'];
     1243        }
     1244
     1245        return [];
     1246    }
     1247
     1248    /**
     1249     * Get plugins that depend on a specific plugin
     1250     *
     1251     * @param string $plugin_slug Plugin slug
     1252     * @return array Array of dependent plugin slugs
     1253     */
     1254    public static function get_plugin_dependents( $plugin_slug ) {
     1255        $map = self::get_dependency_map();
     1256
     1257        if ( isset( $map[ $plugin_slug ]['plugins_depending'] ) ) {
     1258            return $map[ $plugin_slug ]['plugins_depending'];
     1259        }
     1260
     1261        return [];
     1262    }
     1263
     1264    /**
     1265     * Check if a plugin is a dependency of any other active plugin
     1266     *
     1267     * @param string $plugin_slug Plugin slug to check
     1268     * @return bool True if other plugins depend on this one
     1269     */
     1270    public static function is_required_by_others( $plugin_slug ) {
     1271        $dependents = self::get_plugin_dependents( $plugin_slug );
     1272        return ! empty( $dependents );
     1273    }
     1274
     1275    /**
     1276     * Get the detection source for a plugin's dependencies
     1277     *
     1278     * @param string $plugin_slug Plugin slug
     1279     * @return string Source type (wp_core, header, code, pattern, ecosystem, custom, unknown)
     1280     */
     1281    public static function get_detection_source( $plugin_slug ) {
     1282        $map = self::get_dependency_map();
     1283
     1284        if ( isset( $map[ $plugin_slug ]['source'] ) ) {
     1285            return $map[ $plugin_slug ]['source'];
     1286        }
     1287
     1288        return 'unknown';
    4341289    }
    4351290}
  • samybaxy-hyperdrive/trunk/mu-loader/shypdr-mu-loader.php

    r3451625 r3454875  
    3434if (!defined('SHYPDR_MU_LOADER_ACTIVE')) {
    3535    define('SHYPDR_MU_LOADER_ACTIVE', true);
    36     define('SHYPDR_MU_LOADER_VERSION', '6.0.1'); // Optimized: Removed excessive reverse deps, streamlined checkout detection
     36    define('SHYPDR_MU_LOADER_VERSION', '6.0.2'); // Enhanced: WP 6.5+ Plugin Dependencies integration, circular dep protection
    3737}
    3838
     
    679679
    680680    /**
    681      * Resolve plugin dependencies (O(k) where k = required plugins)
     681     * Resolve plugin dependencies with circular dependency protection
     682     *
     683     * Time Complexity: O(k + e) where k = plugins to process, e = edges traversed
     684     * Space Complexity: O(k) for the queue and result set
     685     *
     686     * Uses database-stored dependency map when available (built by main plugin),
     687     * falls back to static hardcoded maps for performance when DB isn't ready.
    682688     */
    683689    private static function resolve_dependencies($required_slugs, $active_plugins) {
    684         // Dependency map
    685         static $dependencies = [
     690        // Try to get dependency map from database first (built by main plugin)
     691        // This includes WP 6.5+ Requires Plugins header data
     692        static $db_dependency_map = null;
     693        static $db_circular_deps = null;
     694
     695        if (null === $db_dependency_map) {
     696            global $wpdb;
     697            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     698            $result = $wpdb->get_var(
     699                $wpdb->prepare(
     700                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
     701                    'shypdr_dependency_map'
     702                )
     703            );
     704            if ($result) {
     705                $db_dependency_map = maybe_unserialize($result);
     706            }
     707            if (!is_array($db_dependency_map)) {
     708                $db_dependency_map = [];
     709            }
     710
     711            // Also get circular dependencies
     712            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     713            $circular_result = $wpdb->get_var(
     714                $wpdb->prepare(
     715                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
     716                    'shypdr_circular_dependencies'
     717                )
     718            );
     719            if ($circular_result) {
     720                $db_circular_deps = maybe_unserialize($circular_result);
     721            }
     722            if (!is_array($db_circular_deps)) {
     723                $db_circular_deps = [];
     724            }
     725        }
     726
     727        // Fallback static dependency map (used when DB map is empty)
     728        static $fallback_dependencies = [
    686729            'jet-menu' => ['jet-engine'],
    687730            'jet-blocks' => ['jet-engine'],
     
    733776        // Reverse dependencies (when parent is loaded, also load these children if active)
    734777        // NOTE: Payment gateways are NOT here - they're loaded via direct detection on checkout pages only
    735         static $reverse_deps = [
     778        static $fallback_reverse_deps = [
    736779            'jet-engine' => [
    737780                'jet-menu', 'jet-blocks', 'jet-theme-core', 'jet-elements', 'jet-tabs',
     
    748791        ];
    749792
     793        // Build circular deps set for O(1) lookup
     794        $circular_set = [];
     795        foreach ($db_circular_deps as $pair) {
     796            if (is_array($pair) && count($pair) >= 2) {
     797                $circular_set[$pair[0] . '|' . $pair[1]] = true;
     798                $circular_set[$pair[1] . '|' . $pair[0]] = true;
     799            }
     800        }
     801
    750802        // Build active plugins set for O(1) lookup
    751803        $active_set = [];
     
    757809        $to_load = [];
    758810        $queue = $required_slugs;
    759 
    760         while (!empty($queue)) {
     811        $max_iterations = 1000; // Safety limit to prevent infinite loops
     812        $iterations = 0;
     813
     814        while (!empty($queue) && $iterations < $max_iterations) {
     815            $iterations++;
    761816            $slug = array_shift($queue);
    762817
     
    767822            $to_load[$slug] = true;
    768823
    769             // Add dependencies
    770             if (isset($dependencies[$slug])) {
    771                 foreach ($dependencies[$slug] as $dep) {
    772                     if (!isset($to_load[$dep])) {
    773                         $queue[] = $dep;
    774                     }
     824            // Get dependencies from DB map first, then fallback
     825            $deps = [];
     826            $rdeps = [];
     827
     828            if (!empty($db_dependency_map[$slug])) {
     829                // Use database-stored dependencies (includes WP 6.5+ header data)
     830                $deps = $db_dependency_map[$slug]['depends_on'] ?? [];
     831                $rdeps = $db_dependency_map[$slug]['plugins_depending'] ?? [];
     832            } else {
     833                // Fallback to static map
     834                $deps = $fallback_dependencies[$slug] ?? [];
     835                $rdeps = $fallback_reverse_deps[$slug] ?? [];
     836            }
     837
     838            // Add direct dependencies
     839            foreach ($deps as $dep) {
     840                // Check for circular dependency before adding
     841                $pair_key = $slug . '|' . $dep;
     842                if (isset($circular_set[$pair_key])) {
     843                    continue; // Skip circular dependency
    775844                }
     845
     846                if (!isset($to_load[$dep])) {
     847                    $queue[] = $dep;
     848                }
    776849            }
    777850
    778851            // Add reverse dependencies (if active)
    779             if (isset($reverse_deps[$slug])) {
    780                 foreach ($reverse_deps[$slug] as $rdep) {
    781                     if (!isset($to_load[$rdep]) && isset($active_set[$rdep])) {
     852            foreach ($rdeps as $rdep) {
     853                if (!isset($to_load[$rdep]) && isset($active_set[$rdep])) {
     854                    // Check for circular dependency
     855                    $pair_key = $slug . '|' . $rdep;
     856                    if (!isset($circular_set[$pair_key])) {
    782857                        $queue[] = $rdep;
    783858                    }
  • samybaxy-hyperdrive/trunk/readme.txt

    r3453775 r3454875  
    55Requires at least: 6.4
    66Tested up to: 6.9
    7 Stable tag: 6.0.1
     7Stable tag: 6.0.2
    88Requires PHP: 8.2
    99License: GPLv2 or later
     
    1515
    1616**Status:** Production Ready
    17 **Current Version:** 6.0.1
     17**Current Version:** 6.0.2
    1818
    1919Samybaxy's Hyperdrive makes WordPress sites **65-75% faster** by intelligently loading only the plugins needed for each page.
     
    8383* **Filter overhead:** < 2.5ms per request
    8484* **Server cost reduction:** 60-70% for same traffic
     85
     86= What's New in v6.0.2 =
     87
     88🔗 **WordPress 6.5+ Plugin Dependencies Integration**
     89
     90Full integration with WordPress core's plugin dependency system:
     91
     92* **WP_Plugin_Dependencies API** - Native support for WordPress 6.5+ dependency tracking
     93* **Requires Plugins Header** - Automatic parsing of the official plugin dependency header
     94* **Circular Dependency Detection** - Prevents infinite loops using DFS algorithm (O(V+E) complexity)
     95* **5-Layer Detection** - WP Core → Header → Code Analysis → Pattern Matching → Known Ecosystems
     96* **wp_plugin_dependencies_slug Filter** - Support for premium/free plugin slug swapping
     97
     98**Technical Improvements:**
     99* Database-backed dependency map for MU-loader
     100* Automatic map rebuild on plugin activation/deactivation
     101* Version upgrade detection with automatic updates
     102* Extended pattern detection for more plugins
    85103
    86104= What's New in v6.0.1 =
     
    287305
    288306== Changelog ==
     307
     308= 6.0.2 - February 5, 2026 =
     309🔗 WordPress 6.5+ Plugin Dependencies Integration
     310* ✨ New: Full integration with WordPress 6.5+ WP_Plugin_Dependencies API
     311* ✨ New: Native support for Requires Plugins header parsing
     312* ✨ New: Circular dependency detection using DFS with three-color marking
     313* ✨ New: Proper slug validation matching WordPress.org format
     314* ✨ New: Support for wp_plugin_dependencies_slug filter (premium/free plugin swapping)
     315* ✨ New: 5-layer dependency detection hierarchy
     316* 🔧 Improved: MU-loader now uses database-stored dependency map
     317* 🔧 Improved: Automatic dependency map rebuild on plugin changes
     318* 🔧 Improved: Version upgrade detection with automatic MU-loader updates
     319* 🛡️ Safety: Circular dependency protection prevents infinite loops
     320* 🛡️ Safety: Max iteration limit as additional protection
    289321
    290322= 6.0.1 - February 1, 2026 =
  • samybaxy-hyperdrive/trunk/samybaxy-hyperdrive.php

    r3451625 r3454875  
    44 * Plugin URI: https://github.com/samybaxy/samybaxy-hyperdrive
    55 * Description: Revolutionary plugin filtering - Load only essential plugins per page. Requires MU-plugin loader for actual performance gains.
    6  * Version: 6.0.1
     6 * Version: 6.0.2
    77 * Author: samybaxy
    88 * Author URI: https://github.com/samybaxy
     
    2222
    2323// Core initialization constants
    24 define('SHYPDR_VERSION', '6.0.1');
     24define('SHYPDR_VERSION', '6.0.2');
    2525define('SHYPDR_DIR', plugin_dir_path(__FILE__));
    2626define('SHYPDR_URL', plugin_dir_url(__FILE__));
     
    5555    // and prevents old MU-loaders from interfering with activation
    5656    shypdr_install_mu_loader();
    57 }
     57
     58    // Rebuild dependency map on activation (includes WP 6.5+ header support)
     59    if (class_exists('SHYPDR_Dependency_Detector')) {
     60        SHYPDR_Dependency_Detector::rebuild_dependency_map();
     61    }
     62
     63    // Store current version for upgrade detection
     64    update_option('shypdr_version', SHYPDR_VERSION);
     65}
     66
     67/**
     68 * Check for plugin version upgrade and run migrations
     69 */
     70function shypdr_check_version_upgrade() {
     71    $stored_version = get_option('shypdr_version', '0');
     72
     73    if (version_compare($stored_version, SHYPDR_VERSION, '<')) {
     74        // Version upgrade detected - rebuild dependency map
     75        // This ensures WP 6.5+ Requires Plugins header data is picked up
     76        if (class_exists('SHYPDR_Dependency_Detector')) {
     77            SHYPDR_Dependency_Detector::rebuild_dependency_map();
     78        }
     79
     80        // Update MU-loader to latest version
     81        shypdr_install_mu_loader();
     82
     83        // Store new version
     84        update_option('shypdr_version', SHYPDR_VERSION);
     85    }
     86}
     87add_action('admin_init', 'shypdr_check_version_upgrade');
    5888
    5989/**
Note: See TracChangeset for help on using the changeset viewer.