owner descriptor. Longest prefix wins. * * @var array */ private $prefixes = array(); /** * URL path root => folder, read on first use. * * @var array|null */ private $url_roots = null; /** * File => owner key cache. * * @var array */ private $file_cache = array(); /** * Owner descriptors by key. * * @var array */ private $owners = array(); /** * Keys that belong to the profiler itself (plugin + MU loader); never measured or reported. * * @var string[] */ private $self_keys = array(); /** * Core owner key. */ const CORE = 'core'; /** * Unknown owner key. */ const UNKNOWN = 'unknown'; /** * Constructor builds the prefix map from the installed plugins/themes. * * @param array $overrides Directory (absolute) => owner key; keys without a slash are option-name prefixes and ignored here. */ public function __construct( array $overrides = array() ) { // Raw keys only: the constructor runs from the MU loader before translations may load. See label(). $this->owners[ self::CORE ] = array( 'key' => self::CORE, 'type' => 'core', 'name' => self::CORE, ); $this->owners[ self::UNKNOWN ] = array( 'key' => self::UNKNOWN, 'type' => 'unknown', 'name' => self::UNKNOWN, ); $plugin_dir = self::norm( WP_PLUGIN_DIR ); $mu_dir = self::norm( WPMU_PLUGIN_DIR ); $plugins = self::plugin_files(); $active = array_flip( self::active_files() ); foreach ( $plugins as $file => $name ) { $parts = explode( '/', $file ); $key = 'plugin:' . ( count( $parts ) > 1 ? $parts[0] : preg_replace( '/\.php$/', '', $parts[0] ) ); if ( isset( $this->owners[ $key ] ) ) { continue; // A second plugin file in the same folder: the active one (listed first) owns it. } $this->owners[ $key ] = array( 'key' => $key, 'type' => 'plugin', 'name' => $name, 'file' => $file, ); if ( ! isset( $active[ $file ] ) ) { // Installed but not active: only leftover options and cron events can belong to it. $this->owners[ $key ]['type'] = 'inactive'; $this->owners[ $key ]['installed'] = true; } $path = count( $parts ) > 1 ? $plugin_dir . '/' . $parts[0] . '/' : $plugin_dir . '/' . $parts[0]; $this->prefixes[ $path ] = $this->owners[ $key ]; } if ( is_dir( $mu_dir ) ) { foreach ( (array) glob( $mu_dir . '/*' ) as $entry ) { $base = basename( (string) $entry ); $key = 'mu:' . preg_replace( '/\.php$/', '', $base ); $this->owners[ $key ] = array( 'key' => $key, 'type' => 'mu', 'name' => 'MU: ' . $base, ); $this->prefixes[ self::norm( (string) $entry ) . ( is_dir( (string) $entry ) ? '/' : '' ) ] = $this->owners[ $key ]; } } $theme = wp_get_theme(); if ( $theme->exists() ) { $this->add_theme( $theme, 'theme' ); $parent = $theme->parent(); if ( $parent ) { $this->add_theme( $parent, 'theme' ); } } foreach ( $overrides as $dir => $key ) { if ( ! preg_match( '#[/\\\\]#', (string) $dir ) ) { continue; // An option-name prefix (see AutoloadAnalyzer::prefix_overrides()), not a directory. } $dir = self::norm( (string) $dir ); if ( '' === $dir || ! isset( $this->owners[ $key ] ) ) { continue; } $this->prefixes[ rtrim( $dir, '/' ) . '/' ] = $this->owners[ $key ]; } uksort( $this->prefixes, static function ( $a, $b ) { return strlen( $b ) <=> strlen( $a ); } ); $self_files = array( BLOATWRIGHT_DIR . 'bloatwright.php', rtrim( $mu_dir, '/' ) . '/' . \Bloatwright\Services\LoaderInstaller::FILE, rtrim( $mu_dir, '/' ) . '/' . \Bloatwright\Services\LoaderInstaller::LEGACY_FILE, ); foreach ( $self_files as $self ) { $key = $this->for_file( $self ); if ( isset( $this->owners[ $key ] ) && in_array( $this->owners[ $key ]['type'], array( 'plugin', 'mu' ), true ) ) { $this->self_keys[] = $key; } } } /** * Is this the profiler's own code? * * @param string $key Owner key. */ public function is_self( string $key ): bool { return in_array( $key, $this->self_keys, true ); } /** * Keys owned by the profiler. * * @return string[] */ public function self_keys(): array { return $this->self_keys; } /** * Files of the plugins active on this site (network-activated ones included), plus the plugin a * "try without" run filters out of the profiled request: it is active on the site, just not loaded. * * @return string[] */ private static function active_files(): array { $all = (array) get_option( 'active_plugins', array() ); if ( is_multisite() ) { $all = array_merge( $all, array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) ); } if ( defined( 'BLOATWRIGHT_PROFILING_EXCLUDE' ) && constant( 'BLOATWRIGHT_PROFILING_EXCLUDE' ) ) { $all[] = (string) constant( 'BLOATWRIGHT_PROFILING_EXCLUDE' ); } return array_values( array_unique( array_map( 'strval', $all ) ) ); } /** * Plugin files: the active ones (no dependence on get_plugins() so it works in the MU loader), * then, where get_plugins() is loaded, the installed but inactive ones. * * @return array plugin file => name */ public static function plugin_files(): array { $out = array(); foreach ( self::active_files() as $file ) { $file = (string) $file; if ( 0 !== validate_file( $file ) ) { continue; } $path = WP_PLUGIN_DIR . '/' . $file; $name = self::header_name( $path ); if ( '' === $name ) { $name = '.' !== dirname( $file ) ? dirname( $file ) : basename( $file, '.php' ); } $out[ $file ] = $name; } if ( function_exists( 'get_plugins' ) ) { foreach ( get_plugins() as $file => $data ) { if ( ! isset( $out[ $file ] ) ) { $out[ $file ] = (string) ( $data['Name'] ?? $file ); } } } return $out; } /** * Plugin Name header without loading the plugin API. * * @param string $path File. */ private static function header_name( string $path ): string { if ( ! is_readable( $path ) ) { return ''; } $head = (string) file_get_contents( $path, false, null, 0, 8192 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin header read. if ( preg_match( '/^[ \t\/*#@]*Plugin Name:\s*(.+)$/mi', $head, $m ) ) { return trim( (string) $m[1] ); } return ''; } /** * Add a theme owner. * * @param \WP_Theme $theme Theme. * @param string $type Type. */ private function add_theme( \WP_Theme $theme, string $type ): void { $key = 'theme:' . $theme->get_stylesheet(); $this->owners[ $key ] = array( 'key' => $key, 'type' => $type, 'name' => (string) $theme->get( 'Name' ), ); $this->prefixes[ self::norm( $theme->get_stylesheet_directory() ) . '/' ] = $this->owners[ $key ]; } /** * Normalise a path. * * @param string $path Path. */ public static function norm( string $path ): string { $path = str_replace( '\\', '/', $path ); $real = realpath( $path ); if ( false !== $real ) { $path = str_replace( '\\', '/', $real ); } return rtrim( $path, '/' ); } /** * Owner key for a file path. * * @param string $file File. */ public function for_file( string $file ): string { if ( '' === $file ) { return self::UNKNOWN; } if ( isset( $this->file_cache[ $file ] ) ) { return $this->file_cache[ $file ]; } $norm = str_replace( '\\', '/', $file ); $real = false === strpos( $norm, '?' ) && strlen( $norm ) < 4096 ? realpath( $norm ) : false; if ( false !== $real ) { $norm = str_replace( '\\', '/', $real ); } $key = null; foreach ( $this->prefixes as $prefix => $owner ) { if ( 0 === strpos( $norm, $prefix ) ) { $key = $owner['key']; break; } } if ( null === $key ) { $filtered = apply_filters( 'bloatwright_owner_for_file', null, $file ); if ( is_string( $filtered ) && isset( $this->owners[ $filtered ] ) ) { $key = $filtered; } elseif ( '' !== Paths::content() && 0 === strpos( $norm, Paths::content() . '/' ) ) { $key = self::UNKNOWN; // Content folder, but no plugin, MU plugin or theme: drop-ins, uploads, caches. } else { $key = self::CORE; } } $this->file_cache[ $file ] = $key; return $key; } /** * Owner key for a callable. * * @param mixed $callback Callable. */ public function for_callable( $callback ): string { try { if ( $callback instanceof \Closure ) { $ref = new \ReflectionFunction( $callback ); return $this->for_file( (string) $ref->getFileName() ); } if ( is_string( $callback ) ) { if ( false !== strpos( $callback, '::' ) ) { list( $class, $method ) = explode( '::', $callback, 2 ); $ref = new \ReflectionMethod( $class, $method ); } elseif ( function_exists( $callback ) ) { $ref = new \ReflectionFunction( $callback ); } else { return self::UNKNOWN; } return $this->for_file( (string) $ref->getFileName() ); } if ( is_array( $callback ) && 2 === count( $callback ) ) { $target = $callback[0]; $method = (string) $callback[1]; if ( is_object( $target ) || is_string( $target ) ) { if ( is_object( $target ) && ! method_exists( $target, $method ) && method_exists( $target, '__call' ) ) { $ref = new \ReflectionClass( $target ); return $this->for_file( (string) $ref->getFileName() ); } $ref = new \ReflectionMethod( $target, $method ); return $this->for_file( (string) $ref->getFileName() ); } } if ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { $ref = new \ReflectionMethod( $callback, '__invoke' ); return $this->for_file( (string) $ref->getFileName() ); } } catch ( \ReflectionException $e ) { return self::UNKNOWN; } return self::UNKNOWN; } /** * Owner for a URL (asset src) by mapping it back to a local path. * * @param string $url URL. */ public function for_url( string $url ): string { $path = (string) wp_parse_url( $url, PHP_URL_PATH ); if ( '' === $path ) { return self::UNKNOWN; } $file = $this->file_for_path( $path ); if ( '' !== $file ) { return $this->for_file( $file ); } foreach ( array( includes_url(), admin_url() ) as $core_url ) { $root = rtrim( (string) wp_parse_url( $core_url, PHP_URL_PATH ), '/' ); if ( '' !== $root && 0 === strpos( $path, $root . '/' ) ) { return self::CORE; } } return self::UNKNOWN; } /** * Local file path for an asset URL, or '' when remote. * * @param string $url URL. */ public function local_path( string $url ): string { $path = (string) wp_parse_url( $url, PHP_URL_PATH ); $host = (string) wp_parse_url( $url, PHP_URL_HOST ); $site_host = (string) wp_parse_url( site_url(), PHP_URL_HOST ); $site_path = rtrim( (string) wp_parse_url( site_url(), PHP_URL_PATH ), '/' ); if ( '' !== $host && strtolower( $host ) !== strtolower( $site_host ) ) { return ''; } $file = $this->file_for_path( $path ); if ( '' !== $file ) { return $file; } if ( '' !== Paths::core() && 0 === strpos( $path, $site_path . '/' ) ) { return Paths::core() . substr( $path, strlen( $site_path ) ); // wp-includes, wp-admin and other files under the site URL. } return ''; } /** * File for a URL path under a plugin, MU plugin, theme, uploads or content URL root, else ''. * Roots are read when first needed, after plugins and the theme have set them up. * * @param string $path URL path. */ private function file_for_path( string $path ): string { if ( null === $this->url_roots ) { $this->url_roots = Paths::url_roots(); } foreach ( $this->url_roots as $root => $dir ) { if ( 0 === strpos( $path, $root . '/' ) ) { return $dir . substr( $path, strlen( $root ) ); } } return ''; } /** * Descriptor for a key. * * @param string $key Key. * @return array{key:string,type:string,name:string} */ public function describe( string $key ): array { if ( isset( $this->owners[ $key ] ) ) { return self::label( $this->owners[ $key ] ); } if ( 0 === strpos( $key, 'plugin:' ) ) { // Known from the option/cron prefix registry, and not an installed plugin. Installed ones are // owners already when get_plugins() is loaded; without it, inactive and missing look the same. $label = ucwords( str_replace( '-', ' ', substr( $key, 7 ) ) ); return array( 'key' => $key, 'type' => 'inactive', 'name' => function_exists( 'get_plugins' ) /* translators: %s: plugin name derived from its folder name */ ? sprintf( __( '%s (not installed)', 'bloatwright' ), $label ) /* translators: %s: plugin name derived from its folder name */ : sprintf( __( '%s (not active)', 'bloatwright' ), $label ), ); } return array( 'key' => $key, 'type' => 'unknown', 'name' => $key, ); } /** * All known owners. * * @return array */ public function all(): array { return array_map( array( __CLASS__, 'label' ), $this->owners ); } /** * Human name for the built-in owners (translated at output time, never at load time). A profiled * request that ends before init (an early exit) keeps the raw key, so no translation is loaded * too early; the aggregator labels it again when the results are built. * * @param array $desc Descriptor. */ public static function label( array $desc ): array { if ( self::CORE === ( $desc['key'] ?? '' ) && self::CORE === ( $desc['name'] ?? '' ) ) { $desc['name'] = 'WordPress core'; } elseif ( self::UNKNOWN === ( $desc['key'] ?? '' ) && self::UNKNOWN === ( $desc['name'] ?? '' ) && did_action( 'init' ) ) { $desc['name'] = __( 'Unknown', 'bloatwright' ); } return $desc; } /** * Register an ad-hoc owner (used when reading historical data). * * @param array $desc Descriptor. */ public function register( array $desc ): void { if ( isset( $desc['key'] ) && ! isset( $this->owners[ $desc['key'] ] ) ) { $this->owners[ $desc['key'] ] = $desc; } } }