| 1 |
<?php // phpcs:disable |
| 2 |
/** |
| 3 |
* Disco |
| 4 |
* |
| 5 |
* @package Disco |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Disco\Engine; |
| 9 |
|
| 10 |
/** |
| 11 |
* Prevents PHP debug notices/warnings from corrupting REST and AJAX JSON responses. |
| 12 |
* |
| 13 |
* When WP_DEBUG and WP_DEBUG_DISPLAY are both true, notices from plugins (e.g. |
| 14 |
* early textdomain loading) are printed before the JSON body, breaking JSON |
| 15 |
* parsing in the browser. We start an output buffer before plugins_loaded fires |
| 16 |
* so all stray output is captured, then discard it cleanly before the response |
| 17 |
* is sent. AJAX handlers call clean() manually before wp_send_json_*. |
| 18 |
*/ |
| 19 |
class OutputBuffer { |
| 20 |
|
| 21 |
/** |
| 22 |
* Start output buffering for REST/AJAX requests. |
| 23 |
* Call this immediately after the Composer autoload is required in disco.php. |
| 24 |
*/ |
| 25 |
public static function start(): void { |
| 26 |
define( 'DISCO_OB_LEVEL', ob_get_level() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound |
| 27 |
|
| 28 |
if ( ! self::is_api_request() ) { |
| 29 |
return; |
| 30 |
} |
| 31 |
|
| 32 |
ob_start(); |
| 33 |
|
| 34 |
// Suppress error display for API requests so notices do not corrupt JSON. |
| 35 |
// Errors continue to be written to debug.log when WP_DEBUG_LOG is enabled. |
| 36 |
@ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed |
| 37 |
|
| 38 |
if ( self::is_rest_request() ) { |
| 39 |
add_filter( 'rest_pre_serve_request', array( __CLASS__, 'handle_rest' ), 1, 4 ); |
| 40 |
} |
| 41 |
} |
| 42 |
|
| 43 |
/** |
| 44 |
* Hooked on rest_pre_serve_request at priority 1. |
| 45 |
* |
| 46 |
* Strategy: |
| 47 |
* Level N = whatever PHP had before disco.php loaded (DISCO_OB_LEVEL) |
| 48 |
* Level N+1 = our buffer, containing stray debug notices |
| 49 |
* Level N+2 = WordPress's dispatch buffer (if WP uses ob_start in serve_request) |
| 50 |
* |
| 51 |
* We pop WP's dispatch buffer, discard our stray-output buffer, open a fresh |
| 52 |
* buffer, and return false so WP echoes the JSON body into the clean buffer. |
| 53 |
* WP's ob_get_clean() then collects only the actual JSON response. |
| 54 |
* |
| 55 |
* @param bool $served Whether the request has already been served. |
| 56 |
* @param \WP_REST_Response $result The response object. |
| 57 |
* @param \WP_REST_Request $request The current REST request. |
| 58 |
* @param \WP_REST_Server $server The REST server instance. |
| 59 |
* @return bool |
| 60 |
*/ |
| 61 |
public static function handle_rest( $served, $result, $request, $server ): bool { |
| 62 |
if ( $served ) { |
| 63 |
return $served; |
| 64 |
} |
| 65 |
|
| 66 |
$initial_level = DISCO_OB_LEVEL; |
| 67 |
|
| 68 |
// Pop WP's dispatch buffer (level N+2) if it exists, preserving its content. |
| 69 |
$wp_dispatch = ob_get_level() > ( $initial_level + 1 ) ? (string) ob_get_clean() : ''; |
| 70 |
|
| 71 |
// Discard our stray-notices buffer (level N+1). |
| 72 |
if ( ob_get_level() > $initial_level ) { |
| 73 |
ob_end_clean(); |
| 74 |
} |
| 75 |
|
| 76 |
// Open a fresh, clean buffer at the level WP expects for its ob_get_clean(). |
| 77 |
ob_start(); |
| 78 |
|
| 79 |
// Restore any output WP produced during dispatch (almost always empty). |
| 80 |
if ( '' !== $wp_dispatch ) { |
| 81 |
echo $wp_dispatch; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 82 |
} |
| 83 |
|
| 84 |
return false; // Let WP echo the JSON body and collect it via ob_get_clean(). |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Discard all output buffered above DISCO_OB_LEVEL. |
| 89 |
* Call this in AJAX handlers before wp_send_json_* to strip stray debug output. |
| 90 |
*/ |
| 91 |
public static function clean(): void { |
| 92 |
if ( ! defined( 'DISCO_OB_LEVEL' ) ) { |
| 93 |
return; |
| 94 |
} |
| 95 |
while ( ob_get_level() > DISCO_OB_LEVEL ) { |
| 96 |
ob_end_clean(); |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
private static function is_api_request(): bool { |
| 101 |
return self::is_ajax_request() || self::is_rest_request(); |
| 102 |
} |
| 103 |
|
| 104 |
private static function is_ajax_request(): bool { |
| 105 |
return defined( 'DOING_AJAX' ) && DOING_AJAX; |
| 106 |
} |
| 107 |
|
| 108 |
private static function is_rest_request(): bool { |
| 109 |
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { |
| 110 |
return true; |
| 111 |
} |
| 112 |
|
| 113 |
if ( empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 114 |
return false; |
| 115 |
} |
| 116 |
|
| 117 |
// REST_REQUEST is not defined yet at plugin-file load time, so we inspect |
| 118 |
// the URI directly. rest_get_url_prefix() respects custom REST base slugs. |
| 119 |
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 120 |
$rest_prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json'; |
| 121 |
|
| 122 |
return false !== strpos( $request_uri, '/' . $rest_prefix . '/' ); |
| 123 |
} |
| 124 |
} |
| 125 |
|