-'; } // 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 = '
[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( $json ) . '
' . \esc_html( $json ) . '
' . \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( $json ) . '
' . \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( $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 = '