PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.11
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.11
1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 1.3.46 1.3.45 1.3.44 All 176 releases
disco / engine / OutputBuffer.php

OutputBuffer.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.11, at engine/OutputBuffer.php

208 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:disable
2 /**
3 * Disco
4 *
5 * @package Disco
6 */
7
8 namespace Disco\Engine;
9
10 /**
11 * Prevents stray output from corrupting REST and AJAX JSON responses.
12 *
13 * Other plugins frequently emit output during an API request: PHP notices
14 * (e.g. "Function _load_textdomain_just_in_time was called incorrectly"),
15 * deprecation warnings, or plain `echo` calls inside `shutdown`/`rest_post_dispatch`.
16 * Anything printed before the JSON body, and anything printed after it, makes the
17 * response unparseable in the browser.
18 *
19 * Strategy:
20 * 1. Open an output buffer before `plugins_loaded` so nothing reaches the client early.
21 * 2. For our own REST routes, render and send the JSON body ourselves, discarding
22 * everything buffered up to that point.
23 * 3. Immediately after the body is flushed, open a buffer that discards everything,
24 * so late output (shutdown hooks, destructors, deprecations) is never appended.
25 *
26 * AJAX handlers call clean() before wp_send_json_* for step 2, and the
27 * `wp_die_ajax_handler` filter performs step 3.
28 */
29 class OutputBuffer {
30
31 /**
32 * REST route prefix owned by this plugin.
33 */
34 private const ROUTE_PREFIX = '/disco/';
35
36 /**
37 * Start output buffering for REST/AJAX requests.
38 * Call this immediately after the Composer autoload is required in disco.php.
39 */
40 public static function start(): void {
41 define( 'DISCO_OB_LEVEL', ob_get_level() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
42
43 if ( ! self::is_api_request() ) {
44 return;
45 }
46
47 ob_start();
48
49 // Suppress error display for API requests so notices do not corrupt JSON.
50 // Errors continue to be written to debug.log when WP_DEBUG_LOG is enabled.
51 @ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed
52
53 if ( self::is_rest_request() ) {
54 add_filter( 'rest_pre_serve_request', array( __CLASS__, 'handle_rest' ), 1, 4 );
55 }
56
57 if ( self::is_ajax_request() ) {
58 // Runs after wp_send_json_* has echoed its body, before the script dies.
59 add_filter( 'wp_die_ajax_handler', array( __CLASS__, 'handle_ajax_die' ), PHP_INT_MAX );
60 }
61 }
62
63 /**
64 * Hooked on rest_pre_serve_request at priority 1.
65 *
66 * For Disco routes we take over serving: discard whatever stray output was
67 * buffered, echo the JSON body, flush it, and then swallow any later output.
68 * For third-party routes we only drop the stray output we captured and let
69 * WordPress serve the response as usual.
70 *
71 * @param bool $served Whether the request has already been served.
72 * @param \WP_HTTP_Response $result The response object.
73 * @param \WP_REST_Request $request The current REST request.
74 * @param \WP_REST_Server $server The REST server instance.
75 * @return bool
76 */
77 public static function handle_rest( $served, $result, $request, $server ): bool {
78 if ( $served ) {
79 return true;
80 }
81
82 // Drop everything buffered so far (notices printed while WP booted/dispatched).
83 self::clean();
84
85 if ( ! $result instanceof \WP_HTTP_Response
86 || ! $request instanceof \WP_REST_Request
87 || ! $server instanceof \WP_REST_Server
88 || ! self::is_disco_route( $request )
89 || null !== $request->get_param( '_jsonp' ) ) {
90 return false; // Let WordPress echo the body itself.
91 }
92
93 if ( 'HEAD' === $request->get_method() ) {
94 self::seal();
95
96 return true;
97 }
98
99 $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( wp_unslash( $_GET['_embed'] ) ) : false; // phpcs:ignore WordPress.Security
100 $data = $server->response_to_data( $result, $embed );
101
102 /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
103 $data = apply_filters( 'rest_pre_echo_response', $data, $server, $request );
104
105 // Filters above may have printed something; drop it before we emit the body.
106 self::clean();
107
108 if ( null === $data || 204 === $result->get_status() ) {
109 self::seal();
110
111 return true;
112 }
113
114 $options = ( defined( 'WP_DEBUG' ) && WP_DEBUG && $request->has_param( '_pretty' ) ) ? JSON_PRETTY_PRINT : 0;
115 $json = wp_json_encode( $data, $options );
116
117 if ( false === $json ) {
118 $json = (string) wp_json_encode(
119 array(
120 'code' => 'rest_encode_error',
121 'message' => json_last_error_msg(),
122 'data' => array( 'status' => 500 ),
123 )
124 );
125 }
126
127 echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
128
129 self::seal();
130
131 return true;
132 }
133
134 /**
135 * Hooked on wp_die_ajax_handler, which fires after wp_send_json_* echoed its body.
136 * Seals the output so late plugin output cannot be appended to the JSON.
137 *
138 * @param callable $handler The wp_die handler.
139 * @return callable
140 */
141 public static function handle_ajax_die( $handler ) {
142 self::seal();
143
144 return $handler;
145 }
146
147 /**
148 * Discard all output buffered above DISCO_OB_LEVEL.
149 * Call this in AJAX handlers before wp_send_json_* to strip stray debug output.
150 */
151 public static function clean(): void {
152 if ( ! defined( 'DISCO_OB_LEVEL' ) ) {
153 return;
154 }
155 while ( ob_get_level() > DISCO_OB_LEVEL ) {
156 ob_end_clean();
157 }
158 }
159
160 /**
161 * Push the response we already echoed to the client, then open a buffer that
162 * throws away everything written afterwards (shutdown hooks, deprecations,
163 * destructors), so nothing can be appended to the JSON body.
164 */
165 private static function seal(): void {
166 while ( defined( 'DISCO_OB_LEVEL' ) && ob_get_level() > DISCO_OB_LEVEL ) {
167 ob_end_flush();
168 }
169
170 flush();
171
172 ob_start(
173 static function () {
174 return '';
175 }
176 );
177 }
178
179 private static function is_disco_route( \WP_REST_Request $request ): bool {
180 return 0 === strpos( (string) $request->get_route(), self::ROUTE_PREFIX );
181 }
182
183 private static function is_api_request(): bool {
184 return self::is_ajax_request() || self::is_rest_request();
185 }
186
187 private static function is_ajax_request(): bool {
188 return defined( 'DOING_AJAX' ) && DOING_AJAX;
189 }
190
191 private static function is_rest_request(): bool {
192 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
193 return true;
194 }
195
196 if ( empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
197 return false;
198 }
199
200 // REST_REQUEST is not defined yet at plugin-file load time, so we inspect
201 // the URI directly. rest_get_url_prefix() respects custom REST base slugs.
202 $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
203 $rest_prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json';
204
205 return false !== strpos( $request_uri, '/' . $rest_prefix . '/' );
206 }
207 }
208