started ) { return; } $this->started = true; // WordPress/REST server may have opened buffers; flush them all so // bytes reach the client as we write them. while ( ob_get_level() > 0 ) { ob_end_flush(); } status_header( 200 ); header( 'Content-Type: text/event-stream; charset=utf-8' ); header( 'Cache-Control: no-store, no-cache, must-revalidate, private' ); header( 'X-Accel-Buffering: no' ); header( 'Connection: keep-alive' ); header( 'X-Robots-Tag: noindex' ); // Padding comment: some proxies hold the first ~1-2KB before forwarding. echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->flush(); } /** * Emit one event. * * @param string $name Event name. * @param array $data JSON payload. */ public function event( string $name, array $data ): void { if ( ! $this->started ) { return; } $json = wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); if ( ! is_string( $json ) ) { return; } // Data must not contain raw newlines (JSON escapes them already). echo 'event: ' . $name . "\n" . 'data: ' . $json . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->flush(); } /** * Close the stream. */ public function end(): void { if ( ! $this->started ) { return; } echo "event: done\ndata: {}\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $this->flush(); } /** * Whether begin() ran. */ public function started(): bool { return $this->started; } /** * Push bytes to the client. */ private function flush(): void { if ( function_exists( 'flush' ) ) { flush(); } } }