diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a4c22e8f1cca1..21d07d5b34ce9 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -702,6 +702,71 @@ function rest_ensure_response( $response ) { return new WP_REST_Response( $response ); } +/** + * Returns a formatted caller location string for REST API debug log entries. + * + * Searches the call stack for the first frame whose file lives inside WP_CONTENT_DIR, + * identifying the plugin or theme that triggered the notice. Used by the REST API + * debug handlers to append actionable location info to error_log() output. + * + * @since 7.1.0 + * @access private + * + * @return string Formatted string such as ' called from my_func() in /path/plugin.php on line 8', + * or ' called from (anonymous function) in /path/plugin.php on line 8' for closures, + * or empty string when no plugin/theme frame is found in the call stack. + */ +function _rest_get_debug_backtrace_caller(): string { + $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); + $normalized_content_dir = trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ); + + foreach ( $backtrace as $i => $frame ) { + if ( ! isset( $frame['file'] ) ) { + continue; + } + + $normalized_file = wp_normalize_path( $frame['file'] ); + if ( ! str_starts_with( $normalized_file, $normalized_content_dir ) ) { + continue; + } + + $location = ' in ' . $normalized_file; + if ( isset( $frame['line'] ) ) { + $location .= ' on line ' . $frame['line']; + } + + /* + * The next frame holds the function that contains the offending call. + * PHP represents closures as '{closure}' (< 8.4) or '{closure:file:line}' (>= 8.4), + * both starting with '{'. These are replaced with a readable label. + */ + $next = $backtrace[ $i + 1 ] ?? null; + if ( $next ) { + if ( str_starts_with( $next['function'], '{' ) ) { + return ' called from (anonymous function)' . $location; + } + + /* + * A call made from a file's global scope has no enclosing function; PHP + * reports the include/require pseudo-function that loaded the file instead. + * In that case only the location is meaningful. + */ + if ( in_array( $next['function'], array( 'require', 'require_once', 'include', 'include_once' ), true ) ) { + return $location; + } + $caller_func = $next['function']; + if ( isset( $next['class'] ) ) { + $caller_func = $next['class'] . $next['type'] . $caller_func; + } + return ' called from ' . $caller_func . '()' . $location; + } + + return $location; + } + + return ''; +} + /** * Handles _deprecated_function() errors. * @@ -712,7 +777,7 @@ function rest_ensure_response( $response ) { * @param string $version Version. */ function rest_handle_deprecated_function( $function_name, $replacement, $version ) { - if ( ! WP_DEBUG || headers_sent() ) { + if ( ! WP_DEBUG ) { return; } if ( ! empty( $replacement ) ) { @@ -723,7 +788,13 @@ function rest_handle_deprecated_function( $function_name, $replacement, $version $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function_name, $version ); } - header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); + if ( ! headers_sent() ) { + header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); + } + + if ( WP_DEBUG_LOG && ( error_reporting() & E_USER_DEPRECATED ) ) { + error_log( 'PHP Deprecated: ' . wp_strip_all_tags( $string ) . _rest_get_debug_backtrace_caller() ); + } } /** @@ -736,7 +807,7 @@ function rest_handle_deprecated_function( $function_name, $replacement, $version * @param string $version Version. */ function rest_handle_deprecated_argument( $function_name, $message, $version ) { - if ( ! WP_DEBUG || headers_sent() ) { + if ( ! WP_DEBUG ) { return; } if ( $message ) { @@ -747,7 +818,13 @@ function rest_handle_deprecated_argument( $function_name, $message, $version ) { $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function_name, $version ); } - header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); + if ( ! headers_sent() ) { + header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); + } + + if ( WP_DEBUG_LOG && ( error_reporting() & E_USER_DEPRECATED ) ) { + error_log( 'PHP Deprecated: ' . wp_strip_all_tags( $string ) . _rest_get_debug_backtrace_caller() ); + } } /** @@ -760,7 +837,7 @@ function rest_handle_deprecated_argument( $function_name, $message, $version ) { * @param string|null $version The version of WordPress where the message was added. */ function rest_handle_doing_it_wrong( $function_name, $message, $version ) { - if ( ! WP_DEBUG || headers_sent() ) { + if ( ! WP_DEBUG ) { return; } @@ -774,7 +851,13 @@ function rest_handle_doing_it_wrong( $function_name, $message, $version ) { $string = sprintf( $string, $function_name, $message ); } - header( sprintf( 'X-WP-DoingItWrong: %s', $string ) ); + if ( ! headers_sent() ) { + header( sprintf( 'X-WP-DoingItWrong: %s', $string ) ); + } + + if ( WP_DEBUG_LOG && ( error_reporting() & E_USER_NOTICE ) ) { + error_log( 'PHP Notice: ' . wp_strip_all_tags( $string ) . _rest_get_debug_backtrace_caller() ); + } } /**