known = $known ?? new KnownEcosystemRelationshipProvider(); } /** @return Edge[] New edges, without modifying the supplied graph. */ public function resolve( Graph $graph ): array { $data = $graph->jsonSerialize(); $nodes = array(); $plugins = array(); $seen = array(); $ids = array(); $edges = array(); foreach ( $data['nodes'] as $node ) { $nodes[ $node['id'] ] = $node; $metadata = (array) $node['metadata']; // MU loaders are not valid targets for WordPress.org plugin dependencies. if ( $node['type'] === 'plugin' && ( $metadata['kind'] ?? null ) === 'standard' && is_string( $metadata['slug'] ?? null ) ) { $plugins[ $metadata['slug'] ][] = $node['id']; } } foreach ( $data['edges'] as $edge ) { $seen[ $this->key( $edge['source'], $edge['target'], $edge['relationship'] ) ] = true; $ids[ $edge['id'] ] = true; } $add = function ( string $source, string $target, string $relationship, string $evidence ) use ( &$edges, &$seen, &$ids, $nodes ): void { if ( $source === $target || ! isset( $nodes[ $source ], $nodes[ $target ] ) ) { return; } $key = $this->key( $source, $target, $relationship ); if ( isset( $seen[ $key ] ) ) { return; } $base = 'edge:resolved:' . hash( 'sha256', $key ); $id = $base; $suffix = 0; while ( isset( $ids[ $id ] ) ) { $id = $base . ':' . ++$suffix; } $edges[] = new Edge( $id, $source, $target, $relationship, 'high', array( 'evidence' => $evidence ) ); $seen[ $key ] = true; $ids[ $id ] = true; }; foreach ( $nodes as $id => $node ) { $metadata = (array) $node['metadata']; if ( isset( $nodes['wordpress:installation'] ) && $nodes['wordpress:installation']['type'] === 'wordpress' ) { if ( in_array( $node['type'], array( 'plugin', 'theme' ), true ) || ( $id === 'system:cron' && $node['type'] === 'cron' ) ) { $add( 'wordpress:installation', $id, 'contains', 'wordpress_core_structure' ); } if ( ( $id === 'system:php' && $node['type'] === 'php' ) || ( $id === 'system:database' && $node['type'] === 'database' ) ) { $add( 'wordpress:installation', $id, 'uses', 'wordpress_core_structure' ); } } if ( $node['type'] === 'theme' && is_string( $metadata['parent_stylesheet'] ?? null ) ) { $target = 'theme:' . $metadata['parent_stylesheet']; if ( isset( $nodes[ $target ] ) && $nodes[ $target ]['type'] === 'theme' ) { $add( $id, $target, 'child_of', 'theme_parent_metadata' ); } } if ( $node['type'] !== 'plugin' ) { continue; } $requires = $metadata['requires_plugins'] ?? array(); foreach ( is_array( $requires ) ? $requires : array() as $slug ) { if ( is_string( $slug ) && isset( $plugins[ $slug ] ) && count( $plugins[ $slug ] ) === 1 ) { $add( $id, $plugins[ $slug ][0], 'depends_on', 'wordpress_requires_plugins' ); } } // Exact known plugin identities, never display-name or prefix matching. if ( $id === 'plugin:elementor-pro/elementor-pro.php' && ( $metadata['kind'] ?? null ) === 'standard' && isset( $plugins['elementor'] ) && $plugins['elementor'] === array( 'plugin:elementor/elementor.php' ) ) { $add( $id, 'plugin:elementor/elementor.php', 'extends', 'known_ecosystem_mapping' ); } // Explicit WooCommerce dependency/minimum-version headers provide evidence. $wc_minimum = $metadata['wc_requires_at_least'] ?? null; $requires_wc = is_array( $requires ) && in_array( 'woocommerce', $requires, true ); if ( ( $requires_wc || ( is_string( $wc_minimum ) && preg_match( '/^\d+(?:\.\d+){1,3}$/D', $wc_minimum ) ) ) && isset( $plugins['woocommerce'] ) && $plugins['woocommerce'] === array( 'plugin:woocommerce/woocommerce.php' ) ) { $add( $id, 'plugin:woocommerce/woocommerce.php', 'extends', $requires_wc ? 'wordpress_requires_plugins' : 'woocommerce_requires_header' ); } } foreach ( $this->known->relationships( $graph ) as $rule ) { $add( $rule['source'], $rule['target'], $rule['relationship'], $rule['evidence'] ); } return $edges; } private function key( string $source, string $target, string $relationship ): string { return serialize( array( $source, $target, $relationship ) ); } }