| @@ -115,8 +115,9 @@ | ||
| 115 | 115 | 'speedycache', |
| 116 | 116 | 'hummingbird', |
| 117 | 117 | 'aruba_hsc', |
| 118 | 118 | 'spc', |
| 119 | + 'groovy_menu', | |
| 119 | 120 | ]; |
| 120 | 121 | /** |
| 121 | 122 | * The current state of the buffer. |
| 122 | 123 | * |
| @@ -392,9 +393,9 @@ | ||
| 392 | 393 | } |
| 393 | 394 | if ( ! wp_doing_ajax() ) { |
| 394 | 395 | return false; |
| 395 | 396 | } |
| 396 | - if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { | |
| 397 | + if ( isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { | |
| 397 | 398 | return false; |
| 398 | 399 | } |
| 399 | 400 | |
| 400 | 401 | return true; |
| @@ -911,22 +912,25 @@ | ||
| 911 | 912 | $this->start_capture_buffer(); |
| 912 | 913 | } |
| 913 | 914 | |
| 914 | 915 | /** |
| 915 | - * Start an output buffer that captures the page HTML. | |
| 916 | + * Start the output buffer that holds the page HTML. | |
| 916 | 917 | * |
| 917 | - * On normal requests the buffer is captured and processed by close_buffer() | |
| 918 | - * at shutdown, outside of PHP's display-handler context, so callbacks hooked | |
| 919 | - * into our filters are free to use output buffering themselves and fatal | |
| 920 | - * errors raised during processing keep their real message instead of being | |
| 921 | - * masked by "Cannot use output buffering in output buffering display handlers". | |
| 918 | + * By default the page is processed by the attached handler when the buffer | |
| 919 | + * is flushed, the way it worked up to 4.2.11. We never flush other buffers | |
| 920 | + * and open no buffer at shutdown, so code that opens a buffer early and reads | |
| 921 | + * it back with ob_get_clean() at shutdown (FacetWP, Groovy Menu) keeps working. | |
| 922 | 922 | * |
| 923 | - * The attached handler is only a fallback for buffers flushed outside of | |
| 924 | - * close_buffer() — third-party force-flush loops, ob_flush() streaming, or | |
| 925 | - * core's wp_ob_end_flush_all() reaching the re-armed buffer. A named method | |
| 926 | - * is used instead of a closure so the buffer can be identified as ours via | |
| 927 | - * ob_get_status()['name']. | |
| 923 | + * When the optml_capture_at_shutdown filter returns true, close_buffer() | |
| 924 | + * captures and processes the buffer at shutdown instead, outside of PHP's | |
| 925 | + * display-handler context. Callbacks hooked into our filters can then use | |
| 926 | + * output buffering themselves, and fatal errors raised during processing | |
| 927 | + * keep their real message instead of being masked by "Cannot use output | |
| 928 | + * buffering in output buffering display handlers". | |
| 928 | 929 | * |
| 930 | + * A named method is used instead of a closure so the buffer can be | |
| 931 | + * identified as ours via ob_get_status()['name']. | |
| 932 | + * | |
| 929 | 933 | * @return void |
| 930 | 934 | */ |
| 931 | 935 | private function start_capture_buffer() { |
| 932 | 936 | self::$ob_processed = false; |
| @@ -939,18 +943,38 @@ | ||
| 939 | 943 | */ |
| 940 | 944 | const OB_HANDLER_NAME = 'Optml_Manager::handle_buffer_fallback'; |
| 941 | 945 | |
| 942 | 946 | /** |
| 943 | - * Output-buffer handler attached to our capture buffer. | |
| 947 | + * Whether the page is captured and processed at shutdown, outside of PHP's display-handler context. | |
| 944 | 948 | * |
| 945 | - * Runs only when the buffer is flushed outside of close_buffer(). Content is | |
| 946 | - * passed through UNPROCESSED here: running the replacement filter graph | |
| 947 | - * inside a PHP display handler would turn any third-party ob_*() call into | |
| 948 | - * an uncatchable fatal ("Cannot use output buffering in output buffering | |
| 949 | - * display handlers") — the very crash this rework removes. The only | |
| 950 | - * exception is the legacy mode selected via the optml_capture_at_shutdown | |
| 951 | - * filter, which explicitly restores the previous in-handler processing. | |
| 949 | + * @return bool | |
| 950 | + */ | |
| 951 | + private function captures_at_shutdown() { | |
| 952 | + /** | |
| 953 | + * Filters whether the page is captured and processed at shutdown, outside | |
| 954 | + * of PHP's display-handler context, instead of inside the output-buffer | |
| 955 | + * handler. | |
| 956 | + * | |
| 957 | + * Off by default: the shutdown capture flushes the buffers | |
| 958 | + * stacked above ours and opens a new buffer afterwards, which breaks code | |
| 959 | + * that reads its own buffer back at shutdown. Return true to opt in. | |
| 960 | + * | |
| 961 | + * @param bool $capture_at_shutdown Whether to process the buffer at shutdown. | |
| 962 | + */ | |
| 963 | + return apply_filters( 'optml_capture_at_shutdown', false ) === true; | |
| 964 | + } | |
| 965 | + | |
| 966 | + /** | |
| 967 | + * Output-buffer handler attached to our buffer. | |
| 952 | 968 | * |
| 969 | + * By default this is where the page is processed. An exception thrown by the | |
| 970 | + * replacement never breaks the page: the content is returned untouched. | |
| 971 | + * | |
| 972 | + * With the optml_capture_at_shutdown opt-in the handler runs only when the | |
| 973 | + * buffer is flushed outside of close_buffer(), and the content is passed | |
| 974 | + * through unprocessed, because running the replacement filter graph inside a | |
| 975 | + * display handler is what that mode exists to avoid. | |
| 976 | + * | |
| 953 | 977 | * @param string $content The buffered content. |
| 954 | 978 | * @param int $phase PHP's output-handler phase bitmask (unused; keeps replace_content()'s $partial parameter shielded from it). |
| 955 | 979 | * |
| 956 | 980 | * @return string The content to output. |
| @@ -958,9 +982,9 @@ | ||
| 958 | 982 | public function handle_buffer_fallback( $content, $phase = 0 ) { |
| 959 | 983 | if ( self::$ob_processed || $content === '' ) { |
| 960 | 984 | return $content; |
| 961 | 985 | } |
| 962 | - if ( apply_filters( 'optml_capture_at_shutdown', true ) === false ) { | |
| 986 | + if ( ! $this->captures_at_shutdown() ) { | |
| 963 | 987 | try { |
| 964 | 988 | return $this->replace_content( $content, self::is_ajax_request() ); |
| 965 | 989 | } catch ( Throwable $t ) { |
| 966 | 990 | // Never break the page from inside a display handler. |
| @@ -980,16 +1004,9 @@ | ||
| 980 | 1004 | if ( ! self::$ob_started ) { |
| 981 | 1005 | return; |
| 982 | 1006 | } |
| 983 | 1007 | |
| 984 | - /** | |
| 985 | - * Filters whether the captured page is processed at shutdown, outside of | |
| 986 | - * PHP's display-handler context. Return false to restore the legacy | |
| 987 | - * behavior of processing inside the output-buffer handler. | |
| 988 | - * | |
| 989 | - * @param bool $capture_at_shutdown Whether to process the buffer at shutdown. | |
| 990 | - */ | |
| 991 | - if ( apply_filters( 'optml_capture_at_shutdown', true ) === false ) { | |
| 1008 | + if ( ! $this->captures_at_shutdown() ) { | |
| 992 | 1009 | if ( ob_get_length() ) { |
| 993 | 1010 | ob_end_flush(); |
| 994 | 1011 | } |
| 995 | 1012 | return; |
| @@ -1025,12 +1042,12 @@ | ||
| 1025 | 1042 | * |
| 1026 | 1043 | * @return void |
| 1027 | 1044 | */ |
| 1028 | 1045 | public function close_final_buffer() { |
| 1029 | - if ( ! self::$ob_started ) { | |
| 1046 | + if ( ! self::$ob_started || ! $this->captures_at_shutdown() ) { | |
| 1030 | 1047 | return; |
| 1031 | 1048 | } |
| 1032 | - $this->capture_and_process_buffer(); | |
| 1049 | + $this->capture_and_process_buffer( false ); | |
| 1033 | 1050 | } |
| 1034 | 1051 | |
| 1035 | 1052 | /** |
| 1036 | 1053 | * Capture our buffer, process it outside the display-handler context and echo the result. |
| @@ -1038,11 +1055,13 @@ | ||
| 1038 | 1055 | * Ownership is verified by both nesting level and handler identity, so a |
| 1039 | 1056 | * buffer another plugin opened at the same level after ours was closed is |
| 1040 | 1057 | * never captured or closed by us. |
| 1041 | 1058 | * |
| 1059 | + * @param bool $is_page Whether this is the page capture (true) or the late shutdown output (false). | |
| 1060 | + * | |
| 1042 | 1061 | * @return bool Whether our buffer was found and consumed. |
| 1043 | 1062 | */ |
| 1044 | - private function capture_and_process_buffer() { | |
| 1063 | + private function capture_and_process_buffer( $is_page = true ) { | |
| 1045 | 1064 | if ( self::$ob_level === 0 || ob_get_level() !== self::$ob_level ) { |
| 1046 | 1065 | return false; |
| 1047 | 1066 | } |
| 1048 | 1067 | $status = ob_get_status(); |
| @@ -1053,8 +1072,20 @@ | ||
| 1053 | 1072 | // Set before ob_end_clean() so our handler no-ops during buffer cleanup. |
| 1054 | 1073 | self::$ob_processed = true; |
| 1055 | 1074 | ob_end_clean(); |
| 1056 | 1075 | if ( $html !== false && $html !== '' ) { |
| 1076 | + if ( $is_page ) { | |
| 1077 | + /** | |
| 1078 | + * Filters the captured page HTML before Optimole processes it. | |
| 1079 | + * | |
| 1080 | + * Runs once per request, on the buffer captured at shutdown, outside of | |
| 1081 | + * PHP's display-handler context. Late output echoed by other shutdown | |
| 1082 | + * callbacks is not passed through this filter. | |
| 1083 | + * | |
| 1084 | + * @param string $html The full page HTML. | |
| 1085 | + */ | |
| 1086 | + $html = apply_filters( 'optml_captured_page_html', $html ); | |
| 1087 | + } | |
| 1057 | 1088 | echo $this->replace_content( $html, self::is_ajax_request() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- full page HTML, escaping would break the page. |
| 1058 | 1089 | } |
| 1059 | 1090 | return true; |
| 1060 | 1091 | } |