component data. */ private $components = array(); /** @var array Normalized directory => component key, longest first. */ private $roots = null; /** @var array Plugin folder or file => plugin name. */ private $plugin_names = null; /** @var string */ private $abspath; public function __construct() { $this->abspath = trailingslashit( wp_normalize_path( ABSPATH ) ); } /** * Describe one callback. * * @param mixed $callback Callback as stored in WP_Hook::$callbacks. * @return array { label, file, line, component } */ public function describe( $callback ) { $label = $this->label( $callback ); $file = ''; $line = 0; try { $reflection = $this->reflect( $callback ); if ( $reflection ) { $file = (string) $reflection->getFileName(); $line = (int) $reflection->getStartLine(); } } catch ( ReflectionException $e ) { // Callback no longer resolvable (e.g. a method that does not // exist). Keep the label; the file stays unknown. $file = ''; } if ( '' === $file && $this->is_internal( $callback ) ) { $component = $this->component_key( 'php', 'php', __( 'PHP built-in', 'hooktrail' ) ); } else { $component = $this->component_for_file( $file ); } return array( 'label' => $label, 'file' => $this->relative( $file ), 'line' => $line, 'component' => $component, ); } /** * Every component seen so far, keyed as returned by describe(). * * @return array */ public function get_components() { return $this->components; } /** * Readable label for a callback. * * @param mixed $callback Callback. * @return string */ public function label( $callback ) { if ( is_string( $callback ) ) { return $callback; } if ( $callback instanceof Closure ) { return 'Closure'; } if ( is_array( $callback ) && 2 === count( $callback ) ) { list( $target, $method ) = array_values( $callback ); $method = (string) $method; if ( is_object( $target ) ) { return get_class( $target ) . '->' . $method; } if ( is_string( $target ) ) { return $target . '::' . $method; } } if ( is_object( $callback ) ) { return get_class( $callback ) . '->__invoke'; } return __( '(unknown callback)', 'hooktrail' ); } /** * @param mixed $callback Callback. * @return ReflectionFunctionAbstract|null */ private function reflect( $callback ) { if ( $callback instanceof Closure ) { return new ReflectionFunction( $callback ); } if ( is_string( $callback ) ) { if ( false !== strpos( $callback, '::' ) ) { list( $class, $method ) = explode( '::', $callback, 2 ); return new ReflectionMethod( $class, $method ); } return function_exists( $callback ) ? new ReflectionFunction( $callback ) : null; } if ( is_array( $callback ) && 2 === count( $callback ) ) { list( $target, $method ) = array_values( $callback ); $method = (string) $method; // "parent::method" style callbacks. if ( 0 === strpos( $method, 'parent::' ) && is_object( $target ) ) { $parent = get_parent_class( $target ); return $parent ? new ReflectionMethod( $parent, substr( $method, 8 ) ) : null; } if ( ( is_object( $target ) || is_string( $target ) ) && method_exists( $target, $method ) ) { return new ReflectionMethod( $target, $method ); } // Methods reached only through __call / __callStatic. return null; } if ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { return new ReflectionMethod( $callback, '__invoke' ); } return null; } /** * Whether a callback is a PHP built-in function (e.g. 'trim', 'intval'). * * @param mixed $callback Callback. * @return bool */ private function is_internal( $callback ) { if ( ! is_string( $callback ) || ! function_exists( $callback ) ) { return false; } try { $fn = new ReflectionFunction( $callback ); return $fn->isInternal(); } catch ( ReflectionException $e ) { return false; } } /** * Which component a file belongs to. * * @param string $file Absolute path. * @return string Component key. */ public function component_for_file( $file ) { if ( '' === $file ) { return $this->component_key( 'unknown', 'unknown', __( 'Unknown', 'hooktrail' ) ); } $file = wp_normalize_path( $file ); foreach ( $this->get_roots() as $dir => $info ) { if ( 0 !== strpos( $file, $dir ) ) { continue; } if ( 'plugin' === $info['type'] || 'mu-plugin' === $info['type'] ) { // First path segment under the plugins folder is the plugin. $rest = substr( $file, strlen( $dir ) ); $slug = strtok( $rest, '/' ); $name = $this->plugin_name( $slug, $info['type'] ); return $this->component_key( $info['type'], $slug, $name ); } return $this->component_key( $info['type'], $info['slug'], $info['name'] ); } return $this->component_key( 'other', 'other', __( 'Other', 'hooktrail' ) ); } /** * Known root folders, most specific first so a theme inside * wp-content is matched before "other". * * @return array */ private function get_roots() { if ( null !== $this->roots ) { return $this->roots; } $roots = array(); $roots[ trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ) ] = array( 'type' => 'plugin' ); if ( defined( 'WPMU_PLUGIN_DIR' ) ) { $roots[ trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ) ] = array( 'type' => 'mu-plugin' ); } $stylesheet = get_stylesheet(); $template = get_template(); $theme = wp_get_theme(); $roots[ trailingslashit( wp_normalize_path( get_stylesheet_directory() ) ) ] = array( 'type' => 'theme', 'slug' => $stylesheet, 'name' => $theme->get( 'Name' ) ? $theme->get( 'Name' ) : $stylesheet, ); if ( $template !== $stylesheet ) { $parent = wp_get_theme( $template ); $roots[ trailingslashit( wp_normalize_path( get_template_directory() ) ) ] = array( 'type' => 'theme', 'slug' => $template, 'name' => $parent->get( 'Name' ) ? $parent->get( 'Name' ) : $template, ); } $core_name = __( 'WordPress core', 'hooktrail' ); $roots[ $this->abspath . WPINC . '/' ] = array( 'type' => 'core', 'slug' => 'core', 'name' => $core_name, ); $roots[ $this->abspath . 'wp-admin/' ] = array( 'type' => 'core', 'slug' => 'core', 'name' => $core_name, ); // Longest path first, so nested folders win over their parents. uksort( $roots, function ( $a, $b ) { return strlen( $b ) - strlen( $a ); } ); $this->roots = $roots; return $roots; } /** * Plugin display name for a folder (or single-file plugin) name. * * @param string $slug Folder or file name under the plugins directory. * @param string $type 'plugin' or 'mu-plugin'. * @return string */ private function plugin_name( $slug, $type ) { if ( null === $this->plugin_names ) { $this->plugin_names = array(); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } foreach ( get_plugins() as $file => $data ) { $folder = false !== strpos( $file, '/' ) ? strtok( $file, '/' ) : $file; $this->plugin_names[ $folder ] = $data['Name']; } foreach ( get_mu_plugins() as $file => $data ) { $this->plugin_names[ 'mu:' . $file ] = $data['Name']; } } $key = 'mu-plugin' === $type ? 'mu:' . $slug : $slug; return isset( $this->plugin_names[ $key ] ) && '' !== $this->plugin_names[ $key ] ? $this->plugin_names[ $key ] : $slug; } /** * Register a component and return its key. */ private function component_key( $type, $slug, $name ) { $key = $type . ':' . $slug; if ( ! isset( $this->components[ $key ] ) ) { $this->components[ $key ] = array( 'type' => $type, 'slug' => $slug, 'name' => $name, ); } return $key; } /** * Path relative to the WordPress root, for display. * * @param string $file Absolute path. * @return string */ public function relative( $file ) { if ( '' === $file ) { return ''; } $file = wp_normalize_path( $file ); return 0 === strpos( $file, $this->abspath ) ? substr( $file, strlen( $this->abspath ) ) : $file; } }