Changeset 3454875
- Timestamp:
- 02/05/2026 06:10:26 PM (2 months ago)
- Location:
- samybaxy-hyperdrive
- Files:
-
- 8 edited
- 1 copied
-
tags/6.0.2 (copied) (copied from samybaxy-hyperdrive/trunk)
-
tags/6.0.2/includes/class-dependency-detector.php (modified) (25 diffs)
-
tags/6.0.2/mu-loader/shypdr-mu-loader.php (modified) (6 diffs)
-
tags/6.0.2/readme.txt (modified) (4 diffs)
-
tags/6.0.2/samybaxy-hyperdrive.php (modified) (3 diffs)
-
trunk/includes/class-dependency-detector.php (modified) (25 diffs)
-
trunk/mu-loader/shypdr-mu-loader.php (modified) (6 diffs)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/samybaxy-hyperdrive.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
samybaxy-hyperdrive/tags/6.0.2/includes/class-dependency-detector.php
r3451622 r3454875 3 3 * Dependency Detector for Samybaxy's Hyperdrive 4 4 * 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 10 22 * 11 23 * @package SamybaxyHyperdrive 24 * @since 6.0.0 25 * @version 6.0.2 12 26 */ 13 27 … … 24 38 25 39 /** 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 /** 26 72 * Known ecosystem patterns for fallback/validation 73 * These are hardcoded relationships that may not be declared in headers 74 * 75 * @var array 27 76 */ 28 77 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 ], 30 84 'woocommerce' => [ 31 85 'woocommerce-subscriptions', 32 86 'woocommerce-memberships', 87 'woocommerce-product-bundles', 88 'woocommerce-smart-coupons', 33 89 'jet-woo-builder', 90 'jet-woo-product-gallery', 34 91 // Payment gateways - CRITICAL for checkout 35 92 'woocommerce-gateway-stripe', … … 37 94 'stripe', 38 95 'stripe-for-woocommerce', 96 'stripe-payments', 39 97 'woocommerce-payments', 40 98 'woocommerce-paypal-payments', … … 42 100 'paystack', 43 101 ], 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 ], 47 151 ]; 48 152 49 153 /** 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 /** 50 333 * Get the complete dependency map (with caching) 51 334 * 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 * ] 53 346 */ 54 347 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 62 354 $map = get_option( self::DEPENDENCY_MAP_OPTION, false ); 63 355 64 356 // If not found or empty, build it 65 if ( false === $map || empty( $map ) ) {357 if ( false === $map || empty( $map ) || ! is_array( $map ) ) { 66 358 $map = self::build_dependency_map(); 67 359 update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); … … 71 363 $map = apply_filters( 'shypdr_dependency_map', $map ); 72 364 73 $cached_map = $map;365 self::$cached_map = $map; 74 366 return $map; 75 367 } … … 77 369 /** 78 370 * 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 79 381 * 80 382 * @return array Dependency map … … 84 386 $dependency_map = []; 85 387 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 86 394 foreach ( $active_plugins as $plugin_path ) { 87 395 $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 ); 89 397 90 398 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 ); 95 411 } 96 412 } 97 413 98 414 // Build reverse dependencies (who depends on this plugin) 415 // Time: O(n * d) where d = avg dependencies per plugin 99 416 foreach ( $dependency_map as $plugin => $data ) { 100 417 foreach ( $data['depends_on'] as $required_plugin ) { 101 418 if ( ! isset( $dependency_map[ $required_plugin ] ) ) { 102 419 $dependency_map[ $required_plugin ] = [ 103 'depends_on' => [],420 'depends_on' => [], 104 421 'plugins_depending' => [], 422 'source' => 'inferred', 105 423 ]; 106 424 } … … 111 429 } 112 430 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 } 115 446 116 447 return $dependency_map; … … 118 449 119 450 /** 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 121 500 * 122 501 * @param string $plugin_path Plugin file path 502 * @param array $existing_map Existing dependency map to avoid duplicate detection 123 503 * @return array Array of required plugin slugs 124 504 */ 125 private static function detect_plugin_dependencies( $plugin_path ) {505 private static function detect_plugin_dependencies( $plugin_path, $existing_map = [] ) { 126 506 $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path; 507 $slug = self::get_plugin_slug( $plugin_path ); 127 508 $dependencies = []; 128 509 … … 131 512 } 132 513 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) 134 520 $requires_plugins = self::get_requires_plugins_header( $plugin_file ); 135 521 if ( ! empty( $requires_plugins ) ) { … … 149 535 } 150 536 151 // Remove duplicates 537 // Remove duplicates and self-references 152 538 $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. 159 551 * 160 552 * @param string $plugin_file Full path to plugin file 161 * @return array Array of required plugin slugs553 * @return array Array of validated and sanitized plugin slugs 162 554 */ 163 555 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 164 561 $plugin_data = get_plugin_data( $plugin_file, false, false ); 165 562 166 563 // 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 ); 174 623 } 175 624 176 625 /** 177 626 * 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) 178 634 * 179 635 * @param string $plugin_file Full path to plugin file … … 183 639 $dependencies = []; 184 640 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 186 643 $content = file_get_contents( $plugin_file, false, null, 0, 50000 ); 187 644 … … 191 648 192 649 // 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 ) ) { 205 655 $dependencies[] = $plugin_slug; 206 656 } … … 208 658 209 659 // 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 ) { 219 661 if ( false !== strpos( $content, $constant ) ) { 220 662 $dependencies[] = $plugin_slug; … … 222 664 } 223 665 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 ) { 234 668 if ( false !== strpos( $content, $pattern ) ) { 235 669 $dependencies[] = $plugin_slug; … … 250 684 $dependencies = []; 251 685 252 // Pattern : "parent-child" or "parent-addon"686 // Pattern rules: regex => parent plugin 253 687 $patterns = [ 254 // Jet plugins ecosystem 255 '/^jet-(?!engine )/' => 'jet-engine', // jet-* (except jet-engine itself) depends on jet-engine688 // Jet plugins ecosystem (except jet-engine itself) 689 '/^jet-(?!engine$)/' => 'jet-engine', 256 690 257 691 // Elementor ecosystem 258 692 '/^elementor-pro$/' => 'elementor', 259 '/-for-elementor/' => 'elementor', // Addons for Elementor 693 '/-for-elementor/' => 'elementor', 694 '/-elementor-/' => 'elementor', 260 695 261 696 // WooCommerce ecosystem 262 '/^woocommerce-(?!$)/' => 'woocommerce', // woocommerce-* depends on woocommerce 697 '/^woocommerce-(?!$)/' => 'woocommerce', 698 '/^woo-/' => 'woocommerce', 699 '/-for-woocommerce/' => 'woocommerce', 700 '/-woocommerce$/' => 'woocommerce', 263 701 264 702 // LearnPress ecosystem 265 '/^learnpress- (?!$)/' => 'learnpress',703 '/^learnpress-/' => 'learnpress', 266 704 267 705 // Fluent ecosystem 268 706 '/^fluentformpro$/' => 'fluentform', 707 '/^fluent-forms-pro$/' => 'fluentform', 269 708 '/^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', 270 730 ]; 271 731 272 732 foreach ( $patterns as $pattern => $parent_plugin ) { 273 if ( preg_match( $pattern, $slug ) ) {733 if ( preg_match( $pattern, $slug ) && $slug !== $parent_plugin ) { 274 734 $dependencies[] = $parent_plugin; 275 735 } 276 736 } 277 737 278 return $dependencies;738 return array_unique( $dependencies ); 279 739 } 280 740 … … 282 742 * Merge detected dependencies with known ecosystem relationships 283 743 * 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 * 284 747 * @param array $detected_map Detected dependency map 748 * @param array $active_plugins List of active plugin paths 285 749 * @return array Merged dependency map 286 750 */ 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 288 758 foreach ( self::$known_ecosystems as $parent => $children ) { 289 759 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; 295 776 } 296 777 … … 298 779 if ( ! isset( $detected_map[ $parent ] ) ) { 299 780 $detected_map[ $parent ] = [ 300 'depends_on' => [],781 'depends_on' => [], 301 782 'plugins_depending' => [], 783 'source' => 'ecosystem', 302 784 ]; 303 785 } 786 304 787 if ( ! in_array( $child, $detected_map[ $parent ]['plugins_depending'], true ) ) { 305 788 $detected_map[ $parent ]['plugins_depending'][] = $child; … … 312 795 313 796 /** 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 /** 314 1001 * Extract plugin slug from path 315 1002 * 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 328 1025 */ 329 1026 public static function rebuild_dependency_map() { 1027 // Clear all caches 1028 self::$cached_map = null; 1029 self::$circular_deps_cache = null; 330 1030 delete_option( self::DEPENDENCY_MAP_OPTION ); 1031 delete_option( self::CIRCULAR_DEPS_OPTION ); 1032 1033 // Rebuild 331 1034 $map = self::build_dependency_map(); 332 1035 update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); 333 1036 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; 335 1078 } 336 1079 … … 339 1082 */ 340 1083 public static function clear_cache() { 1084 self::$cached_map = null; 1085 self::$circular_deps_cache = null; 341 1086 delete_option( self::DEPENDENCY_MAP_OPTION ); 1087 delete_option( self::CIRCULAR_DEPS_OPTION ); 342 1088 } 343 1089 … … 349 1095 public static function get_stats() { 350 1096 $map = self::get_dependency_map(); 1097 $circular = self::get_circular_dependencies(); 351 1098 352 1099 $total_plugins = count( $map ); 353 1100 $plugins_with_deps = 0; 354 1101 $total_dependencies = 0; 1102 $max_deps = 0; 1103 $plugin_with_most_deps = ''; 355 1104 356 1105 foreach ( $map as $plugin => $data ) { 357 if ( ! empty( $data['depends_on'] ) ) { 1106 $dep_count = count( $data['depends_on'] ?? [] ); 1107 if ( $dep_count > 0 ) { 358 1108 $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 } 360 1114 } 361 1115 } 362 1116 363 1117 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, 366 1120 '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 ), 368 1127 ]; 369 1128 } … … 377 1136 */ 378 1137 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 379 1148 $map = get_option( self::DEPENDENCY_MAP_OPTION, [] ); 380 1149 1150 // Initialize child if needed 381 1151 if ( ! isset( $map[ $child_plugin ] ) ) { 382 1152 $map[ $child_plugin ] = [ 383 'depends_on' => [],1153 'depends_on' => [], 384 1154 'plugins_depending' => [], 1155 'source' => 'custom', 385 1156 ]; 386 1157 } 387 1158 1159 // Add dependency if not exists 388 1160 if ( ! in_array( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ) ) { 389 1161 $map[ $child_plugin ]['depends_on'][] = $parent_plugin; 390 1162 } 391 1163 392 // Update reverse dependency1164 // Initialize parent if needed 393 1165 if ( ! isset( $map[ $parent_plugin ] ) ) { 394 1166 $map[ $parent_plugin ] = [ 395 'depends_on' => [],1167 'depends_on' => [], 396 1168 'plugins_depending' => [], 1169 'source' => 'custom', 397 1170 ]; 398 1171 } 399 1172 1173 // Add reverse dependency if not exists 400 1174 if ( ! in_array( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ) ) { 401 1175 $map[ $parent_plugin ]['plugins_depending'][] = $child_plugin; 402 1176 } 403 1177 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; 404 1189 return update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); 405 1190 } … … 415 1200 $map = get_option( self::DEPENDENCY_MAP_OPTION, [] ); 416 1201 1202 $modified = false; 1203 1204 // Remove from child's depends_on 417 1205 if ( isset( $map[ $child_plugin ]['depends_on'] ) ) { 418 1206 $key = array_search( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ); … … 420 1208 unset( $map[ $child_plugin ]['depends_on'][ $key ] ); 421 1209 $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 425 1215 if ( isset( $map[ $parent_plugin ]['plugins_depending'] ) ) { 426 1216 $key = array_search( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ); … … 428 1218 unset( $map[ $parent_plugin ]['plugins_depending'][ $key ] ); 429 1219 $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'; 434 1289 } 435 1290 } -
samybaxy-hyperdrive/tags/6.0.2/mu-loader/shypdr-mu-loader.php
r3451625 r3454875 34 34 if (!defined('SHYPDR_MU_LOADER_ACTIVE')) { 35 35 define('SHYPDR_MU_LOADER_ACTIVE', true); 36 define('SHYPDR_MU_LOADER_VERSION', '6.0. 1'); // Optimized: Removed excessive reverse deps, streamlined checkout detection36 define('SHYPDR_MU_LOADER_VERSION', '6.0.2'); // Enhanced: WP 6.5+ Plugin Dependencies integration, circular dep protection 37 37 } 38 38 … … 679 679 680 680 /** 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. 682 688 */ 683 689 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 = [ 686 729 'jet-menu' => ['jet-engine'], 687 730 'jet-blocks' => ['jet-engine'], … … 733 776 // Reverse dependencies (when parent is loaded, also load these children if active) 734 777 // 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 = [ 736 779 'jet-engine' => [ 737 780 'jet-menu', 'jet-blocks', 'jet-theme-core', 'jet-elements', 'jet-tabs', … … 748 791 ]; 749 792 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 750 802 // Build active plugins set for O(1) lookup 751 803 $active_set = []; … … 757 809 $to_load = []; 758 810 $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++; 761 816 $slug = array_shift($queue); 762 817 … … 767 822 $to_load[$slug] = true; 768 823 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 775 844 } 845 846 if (!isset($to_load[$dep])) { 847 $queue[] = $dep; 848 } 776 849 } 777 850 778 851 // 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])) { 782 857 $queue[] = $rdep; 783 858 } -
samybaxy-hyperdrive/tags/6.0.2/readme.txt
r3453775 r3454875 5 5 Requires at least: 6.4 6 6 Tested up to: 6.9 7 Stable tag: 6.0. 17 Stable tag: 6.0.2 8 8 Requires PHP: 8.2 9 9 License: GPLv2 or later … … 15 15 16 16 **Status:** Production Ready 17 **Current Version:** 6.0. 117 **Current Version:** 6.0.2 18 18 19 19 Samybaxy's Hyperdrive makes WordPress sites **65-75% faster** by intelligently loading only the plugins needed for each page. … … 83 83 * **Filter overhead:** < 2.5ms per request 84 84 * **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 90 Full 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 85 103 86 104 = What's New in v6.0.1 = … … 287 305 288 306 == 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 289 321 290 322 = 6.0.1 - February 1, 2026 = -
samybaxy-hyperdrive/tags/6.0.2/samybaxy-hyperdrive.php
r3451625 r3454875 4 4 * Plugin URI: https://github.com/samybaxy/samybaxy-hyperdrive 5 5 * Description: Revolutionary plugin filtering - Load only essential plugins per page. Requires MU-plugin loader for actual performance gains. 6 * Version: 6.0. 16 * Version: 6.0.2 7 7 * Author: samybaxy 8 8 * Author URI: https://github.com/samybaxy … … 22 22 23 23 // Core initialization constants 24 define('SHYPDR_VERSION', '6.0. 1');24 define('SHYPDR_VERSION', '6.0.2'); 25 25 define('SHYPDR_DIR', plugin_dir_path(__FILE__)); 26 26 define('SHYPDR_URL', plugin_dir_url(__FILE__)); … … 55 55 // and prevents old MU-loaders from interfering with activation 56 56 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 */ 70 function 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 } 87 add_action('admin_init', 'shypdr_check_version_upgrade'); 58 88 59 89 /** -
samybaxy-hyperdrive/trunk/includes/class-dependency-detector.php
r3451622 r3454875 3 3 * Dependency Detector for Samybaxy's Hyperdrive 4 4 * 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 10 22 * 11 23 * @package SamybaxyHyperdrive 24 * @since 6.0.0 25 * @version 6.0.2 12 26 */ 13 27 … … 24 38 25 39 /** 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 /** 26 72 * Known ecosystem patterns for fallback/validation 73 * These are hardcoded relationships that may not be declared in headers 74 * 75 * @var array 27 76 */ 28 77 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 ], 30 84 'woocommerce' => [ 31 85 'woocommerce-subscriptions', 32 86 'woocommerce-memberships', 87 'woocommerce-product-bundles', 88 'woocommerce-smart-coupons', 33 89 'jet-woo-builder', 90 'jet-woo-product-gallery', 34 91 // Payment gateways - CRITICAL for checkout 35 92 'woocommerce-gateway-stripe', … … 37 94 'stripe', 38 95 'stripe-for-woocommerce', 96 'stripe-payments', 39 97 'woocommerce-payments', 40 98 'woocommerce-paypal-payments', … … 42 100 'paystack', 43 101 ], 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 ], 47 151 ]; 48 152 49 153 /** 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 /** 50 333 * Get the complete dependency map (with caching) 51 334 * 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 * ] 53 346 */ 54 347 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 62 354 $map = get_option( self::DEPENDENCY_MAP_OPTION, false ); 63 355 64 356 // If not found or empty, build it 65 if ( false === $map || empty( $map ) ) {357 if ( false === $map || empty( $map ) || ! is_array( $map ) ) { 66 358 $map = self::build_dependency_map(); 67 359 update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); … … 71 363 $map = apply_filters( 'shypdr_dependency_map', $map ); 72 364 73 $cached_map = $map;365 self::$cached_map = $map; 74 366 return $map; 75 367 } … … 77 369 /** 78 370 * 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 79 381 * 80 382 * @return array Dependency map … … 84 386 $dependency_map = []; 85 387 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 86 394 foreach ( $active_plugins as $plugin_path ) { 87 395 $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 ); 89 397 90 398 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 ); 95 411 } 96 412 } 97 413 98 414 // Build reverse dependencies (who depends on this plugin) 415 // Time: O(n * d) where d = avg dependencies per plugin 99 416 foreach ( $dependency_map as $plugin => $data ) { 100 417 foreach ( $data['depends_on'] as $required_plugin ) { 101 418 if ( ! isset( $dependency_map[ $required_plugin ] ) ) { 102 419 $dependency_map[ $required_plugin ] = [ 103 'depends_on' => [],420 'depends_on' => [], 104 421 'plugins_depending' => [], 422 'source' => 'inferred', 105 423 ]; 106 424 } … … 111 429 } 112 430 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 } 115 446 116 447 return $dependency_map; … … 118 449 119 450 /** 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 121 500 * 122 501 * @param string $plugin_path Plugin file path 502 * @param array $existing_map Existing dependency map to avoid duplicate detection 123 503 * @return array Array of required plugin slugs 124 504 */ 125 private static function detect_plugin_dependencies( $plugin_path ) {505 private static function detect_plugin_dependencies( $plugin_path, $existing_map = [] ) { 126 506 $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path; 507 $slug = self::get_plugin_slug( $plugin_path ); 127 508 $dependencies = []; 128 509 … … 131 512 } 132 513 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) 134 520 $requires_plugins = self::get_requires_plugins_header( $plugin_file ); 135 521 if ( ! empty( $requires_plugins ) ) { … … 149 535 } 150 536 151 // Remove duplicates 537 // Remove duplicates and self-references 152 538 $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. 159 551 * 160 552 * @param string $plugin_file Full path to plugin file 161 * @return array Array of required plugin slugs553 * @return array Array of validated and sanitized plugin slugs 162 554 */ 163 555 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 164 561 $plugin_data = get_plugin_data( $plugin_file, false, false ); 165 562 166 563 // 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 ); 174 623 } 175 624 176 625 /** 177 626 * 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) 178 634 * 179 635 * @param string $plugin_file Full path to plugin file … … 183 639 $dependencies = []; 184 640 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 186 643 $content = file_get_contents( $plugin_file, false, null, 0, 50000 ); 187 644 … … 191 648 192 649 // 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 ) ) { 205 655 $dependencies[] = $plugin_slug; 206 656 } … … 208 658 209 659 // 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 ) { 219 661 if ( false !== strpos( $content, $constant ) ) { 220 662 $dependencies[] = $plugin_slug; … … 222 664 } 223 665 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 ) { 234 668 if ( false !== strpos( $content, $pattern ) ) { 235 669 $dependencies[] = $plugin_slug; … … 250 684 $dependencies = []; 251 685 252 // Pattern : "parent-child" or "parent-addon"686 // Pattern rules: regex => parent plugin 253 687 $patterns = [ 254 // Jet plugins ecosystem 255 '/^jet-(?!engine )/' => 'jet-engine', // jet-* (except jet-engine itself) depends on jet-engine688 // Jet plugins ecosystem (except jet-engine itself) 689 '/^jet-(?!engine$)/' => 'jet-engine', 256 690 257 691 // Elementor ecosystem 258 692 '/^elementor-pro$/' => 'elementor', 259 '/-for-elementor/' => 'elementor', // Addons for Elementor 693 '/-for-elementor/' => 'elementor', 694 '/-elementor-/' => 'elementor', 260 695 261 696 // WooCommerce ecosystem 262 '/^woocommerce-(?!$)/' => 'woocommerce', // woocommerce-* depends on woocommerce 697 '/^woocommerce-(?!$)/' => 'woocommerce', 698 '/^woo-/' => 'woocommerce', 699 '/-for-woocommerce/' => 'woocommerce', 700 '/-woocommerce$/' => 'woocommerce', 263 701 264 702 // LearnPress ecosystem 265 '/^learnpress- (?!$)/' => 'learnpress',703 '/^learnpress-/' => 'learnpress', 266 704 267 705 // Fluent ecosystem 268 706 '/^fluentformpro$/' => 'fluentform', 707 '/^fluent-forms-pro$/' => 'fluentform', 269 708 '/^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', 270 730 ]; 271 731 272 732 foreach ( $patterns as $pattern => $parent_plugin ) { 273 if ( preg_match( $pattern, $slug ) ) {733 if ( preg_match( $pattern, $slug ) && $slug !== $parent_plugin ) { 274 734 $dependencies[] = $parent_plugin; 275 735 } 276 736 } 277 737 278 return $dependencies;738 return array_unique( $dependencies ); 279 739 } 280 740 … … 282 742 * Merge detected dependencies with known ecosystem relationships 283 743 * 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 * 284 747 * @param array $detected_map Detected dependency map 748 * @param array $active_plugins List of active plugin paths 285 749 * @return array Merged dependency map 286 750 */ 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 288 758 foreach ( self::$known_ecosystems as $parent => $children ) { 289 759 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; 295 776 } 296 777 … … 298 779 if ( ! isset( $detected_map[ $parent ] ) ) { 299 780 $detected_map[ $parent ] = [ 300 'depends_on' => [],781 'depends_on' => [], 301 782 'plugins_depending' => [], 783 'source' => 'ecosystem', 302 784 ]; 303 785 } 786 304 787 if ( ! in_array( $child, $detected_map[ $parent ]['plugins_depending'], true ) ) { 305 788 $detected_map[ $parent ]['plugins_depending'][] = $child; … … 312 795 313 796 /** 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 /** 314 1001 * Extract plugin slug from path 315 1002 * 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 328 1025 */ 329 1026 public static function rebuild_dependency_map() { 1027 // Clear all caches 1028 self::$cached_map = null; 1029 self::$circular_deps_cache = null; 330 1030 delete_option( self::DEPENDENCY_MAP_OPTION ); 1031 delete_option( self::CIRCULAR_DEPS_OPTION ); 1032 1033 // Rebuild 331 1034 $map = self::build_dependency_map(); 332 1035 update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); 333 1036 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; 335 1078 } 336 1079 … … 339 1082 */ 340 1083 public static function clear_cache() { 1084 self::$cached_map = null; 1085 self::$circular_deps_cache = null; 341 1086 delete_option( self::DEPENDENCY_MAP_OPTION ); 1087 delete_option( self::CIRCULAR_DEPS_OPTION ); 342 1088 } 343 1089 … … 349 1095 public static function get_stats() { 350 1096 $map = self::get_dependency_map(); 1097 $circular = self::get_circular_dependencies(); 351 1098 352 1099 $total_plugins = count( $map ); 353 1100 $plugins_with_deps = 0; 354 1101 $total_dependencies = 0; 1102 $max_deps = 0; 1103 $plugin_with_most_deps = ''; 355 1104 356 1105 foreach ( $map as $plugin => $data ) { 357 if ( ! empty( $data['depends_on'] ) ) { 1106 $dep_count = count( $data['depends_on'] ?? [] ); 1107 if ( $dep_count > 0 ) { 358 1108 $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 } 360 1114 } 361 1115 } 362 1116 363 1117 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, 366 1120 '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 ), 368 1127 ]; 369 1128 } … … 377 1136 */ 378 1137 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 379 1148 $map = get_option( self::DEPENDENCY_MAP_OPTION, [] ); 380 1149 1150 // Initialize child if needed 381 1151 if ( ! isset( $map[ $child_plugin ] ) ) { 382 1152 $map[ $child_plugin ] = [ 383 'depends_on' => [],1153 'depends_on' => [], 384 1154 'plugins_depending' => [], 1155 'source' => 'custom', 385 1156 ]; 386 1157 } 387 1158 1159 // Add dependency if not exists 388 1160 if ( ! in_array( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ) ) { 389 1161 $map[ $child_plugin ]['depends_on'][] = $parent_plugin; 390 1162 } 391 1163 392 // Update reverse dependency1164 // Initialize parent if needed 393 1165 if ( ! isset( $map[ $parent_plugin ] ) ) { 394 1166 $map[ $parent_plugin ] = [ 395 'depends_on' => [],1167 'depends_on' => [], 396 1168 'plugins_depending' => [], 1169 'source' => 'custom', 397 1170 ]; 398 1171 } 399 1172 1173 // Add reverse dependency if not exists 400 1174 if ( ! in_array( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ) ) { 401 1175 $map[ $parent_plugin ]['plugins_depending'][] = $child_plugin; 402 1176 } 403 1177 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; 404 1189 return update_option( self::DEPENDENCY_MAP_OPTION, $map, false ); 405 1190 } … … 415 1200 $map = get_option( self::DEPENDENCY_MAP_OPTION, [] ); 416 1201 1202 $modified = false; 1203 1204 // Remove from child's depends_on 417 1205 if ( isset( $map[ $child_plugin ]['depends_on'] ) ) { 418 1206 $key = array_search( $parent_plugin, $map[ $child_plugin ]['depends_on'], true ); … … 420 1208 unset( $map[ $child_plugin ]['depends_on'][ $key ] ); 421 1209 $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 425 1215 if ( isset( $map[ $parent_plugin ]['plugins_depending'] ) ) { 426 1216 $key = array_search( $child_plugin, $map[ $parent_plugin ]['plugins_depending'], true ); … … 428 1218 unset( $map[ $parent_plugin ]['plugins_depending'][ $key ] ); 429 1219 $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'; 434 1289 } 435 1290 } -
samybaxy-hyperdrive/trunk/mu-loader/shypdr-mu-loader.php
r3451625 r3454875 34 34 if (!defined('SHYPDR_MU_LOADER_ACTIVE')) { 35 35 define('SHYPDR_MU_LOADER_ACTIVE', true); 36 define('SHYPDR_MU_LOADER_VERSION', '6.0. 1'); // Optimized: Removed excessive reverse deps, streamlined checkout detection36 define('SHYPDR_MU_LOADER_VERSION', '6.0.2'); // Enhanced: WP 6.5+ Plugin Dependencies integration, circular dep protection 37 37 } 38 38 … … 679 679 680 680 /** 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. 682 688 */ 683 689 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 = [ 686 729 'jet-menu' => ['jet-engine'], 687 730 'jet-blocks' => ['jet-engine'], … … 733 776 // Reverse dependencies (when parent is loaded, also load these children if active) 734 777 // 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 = [ 736 779 'jet-engine' => [ 737 780 'jet-menu', 'jet-blocks', 'jet-theme-core', 'jet-elements', 'jet-tabs', … … 748 791 ]; 749 792 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 750 802 // Build active plugins set for O(1) lookup 751 803 $active_set = []; … … 757 809 $to_load = []; 758 810 $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++; 761 816 $slug = array_shift($queue); 762 817 … … 767 822 $to_load[$slug] = true; 768 823 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 775 844 } 845 846 if (!isset($to_load[$dep])) { 847 $queue[] = $dep; 848 } 776 849 } 777 850 778 851 // 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])) { 782 857 $queue[] = $rdep; 783 858 } -
samybaxy-hyperdrive/trunk/readme.txt
r3453775 r3454875 5 5 Requires at least: 6.4 6 6 Tested up to: 6.9 7 Stable tag: 6.0. 17 Stable tag: 6.0.2 8 8 Requires PHP: 8.2 9 9 License: GPLv2 or later … … 15 15 16 16 **Status:** Production Ready 17 **Current Version:** 6.0. 117 **Current Version:** 6.0.2 18 18 19 19 Samybaxy's Hyperdrive makes WordPress sites **65-75% faster** by intelligently loading only the plugins needed for each page. … … 83 83 * **Filter overhead:** < 2.5ms per request 84 84 * **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 90 Full 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 85 103 86 104 = What's New in v6.0.1 = … … 287 305 288 306 == 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 289 321 290 322 = 6.0.1 - February 1, 2026 = -
samybaxy-hyperdrive/trunk/samybaxy-hyperdrive.php
r3451625 r3454875 4 4 * Plugin URI: https://github.com/samybaxy/samybaxy-hyperdrive 5 5 * Description: Revolutionary plugin filtering - Load only essential plugins per page. Requires MU-plugin loader for actual performance gains. 6 * Version: 6.0. 16 * Version: 6.0.2 7 7 * Author: samybaxy 8 8 * Author URI: https://github.com/samybaxy … … 22 22 23 23 // Core initialization constants 24 define('SHYPDR_VERSION', '6.0. 1');24 define('SHYPDR_VERSION', '6.0.2'); 25 25 define('SHYPDR_DIR', plugin_dir_path(__FILE__)); 26 26 define('SHYPDR_URL', plugin_dir_url(__FILE__)); … … 55 55 // and prevents old MU-loaders from interfering with activation 56 56 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 */ 70 function 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 } 87 add_action('admin_init', 'shypdr_check_version_upgrade'); 58 88 59 89 /**
Note: See TracChangeset
for help on using the changeset viewer.