-'; } // If no definitions but we have captured params, show them as-is. if ( empty( $param_defs ) && ! empty( $captured_params ) ) { return self::render_raw_parameters( $captured_params ); } // Render with definitions. $output = '
'; foreach ( $param_defs as $index => $param_def ) { $param_name = isset( $param_def['name'] ) ? $param_def['name'] : ''; $param_type = isset( $param_def['type'] ) ? $param_def['type'] : 'string'; $extraction_code = isset( $param_def['extraction_code'] ) ? $param_def['extraction_code'] : ''; if ( empty( $param_name ) ) { continue; } // Get captured value for this parameter (by index). $value = isset( $captured_params[ $index ] ) ? $captured_params[ $index ] : null; if ( is_null( $value ) ) { $output .= '
' . \esc_html( $param_name ) . ': N/A
'; continue; } $output .= '
' . \esc_html( $param_name ) . ': '; $output .= self::format_value( $value, $param_type, $extraction_code ); $output .= '
'; } $output .= '
'; return $output; } /** * Format a parameter value based on its type. * * @param mixed $value The parameter value. * @param string $type The parameter type. * @param string $extraction_code Custom extraction code. * * @return string Formatted HTML output. * * @since 4.5.0 */ private static function format_value( $value, string $type, string $extraction_code ): string { // Execute custom extraction code if provided. if ( ! empty( $extraction_code ) ) { try { $formatted = self::execute_extraction_code( $value, $extraction_code ); if ( false !== $formatted ) { return $formatted; } } catch ( \Throwable $e ) { return '[Error: ' . \esc_html( $e->getMessage() ) . ']'; } } /** * Sometimes we have type specified, but the actual value is not of this type. * That comes from the fact that anyone is free to pass parameters of any type to hooks. * Usually capturing logic guesses that right, so here we need to try to use the actual value type. * Here we will cover situation only when type is 'string', but value is something else (JSON string). */ if ( is_string( $value ) ) { $decoded = json_decode( $value, true ); // Handle valid JSON (objects or arrays). if ( json_last_error() === JSON_ERROR_NONE ) { $value = $decoded; } elseif ( preg_match( '/^[aOs]:[0-9]+:/', $value ) ) { // Try unserialize if not valid JSON but looks like serialized PHP. $unserialized = @unserialize( $value, array( 'allowed_classes' => false ) ); if ( false !== $unserialized || 'b:0;' === $value ) { $value = $unserialized; } } } if ( \is_array( $value ) && ! \in_array( $type, array( 'array', 'object' ), true ) ) { $type = 'array'; } // Override type if actual value type differs significantly. // Format by type. if ( '' === trim( $type ) ) { if ( is_array( $value ) ) { $type = 'array'; } elseif ( is_object( $value ) ) { $type = 'object'; } elseif ( is_bool( $value ) ) { $type = 'bool'; } elseif ( is_null( $value ) ) { $type = 'null'; } elseif ( is_numeric( $value ) ) { $type = 'int'; } } // Format based on type. switch ( $type ) { case 'user_id': return self::format_user_id( $value ); case 'post_id': return self::format_post_id( $value ); case 'term_id': return self::format_term_id( $value ); case 'comment_id': return self::format_comment_id( $value ); case 'blog_id': return self::format_blog_id( $value ); case 'wp_user': return self::format_wp_user( $value ); case 'wp_post': return self::format_wp_post( $value ); case 'wp_error': return self::format_wp_error( $value ); case 'array': return self::format_array( $value ); case 'object': return self::format_object( $value ); case 'bool': return self::format_bool( $value ); case 'int': case 'float': case 'string': default: return self::format_simple( $value ); } } /** * Execute custom extraction code. * * @param mixed $value The parameter value. * @param string $code The extraction code. * * @return string|false Formatted output or false on failure. * * @throws \Throwable If code execution fails. * @throws \Exception If unsafe code is detected. * * @since 4.5.0 */ private static function execute_extraction_code( $value, string $code ) { try { // Validate the code for safety. if ( ! self::is_safe_code( $code ) ) { throw new \Exception( 'Unsafe extraction code detected.' ); } // Create a safe scope for the code execution. $eval_func = function ( $value ) use ( $code ) { // phpcs:ignore Squiz.PHP.Eval.Discouraged return eval( $code ); }; $result = $eval_func( $value ); if ( is_string( $result ) || is_numeric( $result ) ) { return \esc_html( (string) $result ); } elseif ( is_bool( $result ) ) { return $result ? '✓' : '✗'; } elseif ( is_array( $result ) || is_object( $result ) ) { return '' . \esc_html( \wp_json_encode( $result ) ) . ''; } return false; } catch ( \Throwable $e ) { throw $e; } } /** * Check if the extraction code is safe to execute. * * @param string $code The code to check. * * @return bool True if safe, false otherwise. * * @since 4.5.0 */ private static function is_safe_code( string $code ): bool { // Prepend PHP tag for proper tokenization. $tokens = token_get_all( ' $token ) { if ( is_array( $token ) ) { $token_type = $token[0]; $token_value = $token[1]; // Check for forbidden structural tokens. if ( in_array( $token_type, $forbidden_tokens, true ) ) { // Allow only $value as variable. if ( \T_VARIABLE === $token_type && '$value' === $token_value ) { continue; } return false; } // Check for forbidden function calls: T_STRING immediately followed by "(". if ( \T_STRING === $token_type ) { $next = $index + 1 < $count ? $significant[ $index + 1 ] : null; $is_call = $next !== null && ! is_array( $next ) && '(' === $next; if ( $is_call ) { $func_name = strtolower( $token_value ); if ( in_array( $func_name, $forbidden_functions, true ) ) { return false; } } } } } return true; } /** * Format User ID. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_user_id( $value ): string { $user_id = \absint( $value ); if ( $user_id > 0 ) { $user = \get_user_by( 'ID', $user_id ); if ( $user ) { return sprintf( '%s (#%d)', \esc_url( \get_edit_user_link( $user_id ) ), \esc_html( $user->user_login ), $user_id ); } } return '' . \esc_html( $value ) . ''; } /** * Format Post ID. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_post_id( $value ): string { $post_id = \absint( $value ); if ( $post_id > 0 ) { $post = \get_post( $post_id ); if ( $post ) { $title = $post->post_title ?: $post->post_name ?: '(no title)'; $display_title = self::truncate_text( $title, 20 ); $edit_link = \get_edit_post_link( $post_id ); if ( $edit_link ) { return sprintf( '%s (#%d)', \esc_url( $edit_link ), \esc_attr( $title ), \esc_html( $display_title ), $post_id ); } return sprintf( '%s (#%d)', \esc_html( $display_title ), $post_id ); } } return '' . \esc_html( $value ) . ''; } /** * Format Term ID. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_term_id( $value ): string { $term_id = \absint( $value ); if ( $term_id > 0 ) { $term = \get_term( $term_id ); if ( $term && ! \is_wp_error( $term ) ) { $edit_link = \get_edit_term_link( $term_id, $term->taxonomy ); if ( $edit_link ) { return sprintf( '%s (#%d)', \esc_url( $edit_link ), \esc_html( $term->name ), $term_id ); } return sprintf( '%s (#%d)', \esc_html( $term->name ), $term_id ); } } return '' . \esc_html( $value ) . ''; } /** * Format Comment ID. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_comment_id( $value ): string { $comment_id = \absint( $value ); if ( $comment_id > 0 ) { $comment = \get_comment( $comment_id ); if ( $comment ) { $edit_link = \get_edit_comment_link( $comment_id ); if ( $edit_link ) { return sprintf( 'Comment #%d', \esc_url( $edit_link ), $comment_id ); } return sprintf( 'Comment (#%d)', $comment_id ); } } return '' . \esc_html( $value ) . ''; } /** * Format Blog ID. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_blog_id( $value ): string { if ( ! \is_multisite() ) { return '' . \esc_html( $value ) . ''; } $blog_id = \absint( $value ); if ( $blog_id > 0 ) { $site = \get_site( $blog_id ); if ( $site ) { return sprintf( '%s (#%d)', \esc_url( \get_admin_url( $blog_id ) ), \esc_html( $site->blogname ), $blog_id ); } } return '' . \esc_html( $value ) . ''; } /** * Format WP_User object. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_wp_user( $value ): string { if ( is_object( $value ) && isset( $value->ID ) ) { return self::format_user_id( $value->ID ); } return self::format_object( $value ); } /** * Format WP_Post object. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_wp_post( $value ): string { if ( is_object( $value ) && isset( $value->ID ) ) { return self::format_post_id( $value->ID ); } return self::format_object( $value ); } /** * Format WP_Error object. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_wp_error( $value ): string { if ( \is_wp_error( $value ) ) { return sprintf( 'Error: %s', \esc_html( $value->get_error_message() ) ); } return self::format_object( $value ); } /** * Format array. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_array( $value ): string { if ( is_array( $value ) ) { if ( empty( $value ) ) { return '[]'; } // Check if this array represents an object (has __class__ marker). if ( isset( $value['__class__'] ) && is_string( $value['__class__'] ) ) { $class_name = $value['__class__']; // Remove __class__ from display data. $display_data = $value; unset( $display_data['__class__'] ); $json = \wp_json_encode( $display_data, JSON_PRETTY_PRINT ); $count = count( $display_data ); return '
' . \esc_html( $class_name ) . ' (' . \esc_html( (string) $count ) . ' properties)
' . \esc_html( $json ) . '
'; } // Regular array. $json = \wp_json_encode( $value, JSON_PRETTY_PRINT ); return '
Array (' . count( $value ) . ' items)
' . \esc_html( $json ) . '
'; } return '' . \esc_html( print_r( $value, true ) ) . ''; } /** * Format object. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_object( $value ): string { // If it's an array with __class__ marker, it represents an object stored as array. if ( is_array( $value ) && isset( $value['__class__'] ) && is_string( $value['__class__'] ) ) { return self::format_array( $value ); } if ( is_object( $value ) ) { $class_name = get_class( $value ); $json = \wp_json_encode( $value, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR ); // Try to get object properties count. $props = get_object_vars( $value ); $count = count( $props ); return '
' . \esc_html( $class_name ) . ' (' . \esc_html( (string) $count ) . ' properties)
' . \esc_html( $json ) . '
'; } return '' . \esc_html( print_r( $value, true ) ) . ''; } /** * Format boolean. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_bool( $value ): string { $bool_val = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); if ( is_null( $bool_val ) ) { return '' . \esc_html( (string) $value ) . ''; } return $bool_val ? '✓ true' : '✗ false'; } /** * Format simple types (string, int, float). * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function format_simple( $value ): string { if ( is_scalar( $value ) ) { $str = (string) $value; if ( strlen( $str ) > 100 ) { return '
' . \esc_html( substr( $str, 0, 100 ) ) . '...
' . \esc_html( $str ) . '
'; } return '' . \esc_html( $str ) . ''; } return '' . \esc_html( print_r( (array) $value, true ) ) . ''; } /** * Safely truncate text to a maximum length with ellipsis. * * @param string $text Text to truncate. * @param int $limit Maximum length before truncation. * * @return string */ private static function truncate_text( string $text, int $limit = 20 ): string { $limit = max( 1, $limit ); if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) { if ( mb_strlen( $text ) <= $limit ) { return $text; } return rtrim( mb_substr( $text, 0, $limit ), " \t\n\r\0\x0B" ) . '...'; } if ( strlen( $text ) <= $limit ) { return $text; } return rtrim( substr( $text, 0, $limit ), " \t\n\r\0\x0B" ) . '...'; } /** * Render raw parameters without type definitions. * * @param array $parameters The parameters array. * * @return string Formatted output. * * @since 4.5.0 */ private static function render_raw_parameters( array $parameters ): string { if ( empty( $parameters ) ) { return '-'; } $output = '
'; foreach ( $parameters as $index => $value ) { $output .= '
Param ' . ( $index + 1 ) . ': '; // Try to auto-detect type and format accordingly. $output .= self::auto_format_value( $value ); $output .= '
'; } $output .= '
'; return $output; } /** * Auto-detect value type and format accordingly. * * @param mixed $value The value. * * @return string Formatted output. * * @since 4.5.0 */ private static function auto_format_value( $value ): string { // Check if it's a WP_Error. if ( \is_wp_error( $value ) ) { return self::format_wp_error( $value ); } // Check if it's a WP_User. if ( is_object( $value ) && $value instanceof \WP_User ) { return self::format_wp_user( $value ); } // Check if it's a WP_Post. if ( is_object( $value ) && $value instanceof \WP_Post ) { return self::format_wp_post( $value ); } // Check if it's a numeric ID that might be a user or post. if ( is_numeric( $value ) && $value > 0 ) { // Try to see if it's a valid user ID. $user = \get_user_by( 'ID', (int) $value ); if ( $user ) { return self::format_user_id( $value ) . ' (auto-detected)'; } // Try to see if it's a valid post ID. $post = \get_post( (int) $value ); if ( $post ) { return self::format_post_id( $value ) . ' (auto-detected)'; } } // Boolean. if ( is_bool( $value ) ) { return self::format_bool( $value ); } // Array. if ( is_array( $value ) ) { return self::format_array( $value ); } // Object. if ( is_object( $value ) ) { return self::format_object( $value ); } // Simple scalar. return self::format_simple( $value ); } } }