Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 24 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| Output_Filter | |
0.00% |
0 / 24 |
|
0.00% |
0 / 2 |
72 | |
0.00% |
0 / 1 |
| add_callback | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| tick | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Uses standard output buffering implemented to PHP |
| 4 | * to allow for manipulating the output stream. |
| 5 | * |
| 6 | * The implementation allows for seamless text search and manipulation by |
| 7 | * always taking into account two subsequent chunks of data. |
| 8 | * |
| 9 | * Example sequence of chunks sent to output (chunk size 4): |
| 10 | * |
| 11 | * ABCD EFGH IJKL MNOP QRST |
| 12 | * |
| 13 | * A standard output buffer callback handler would always receive only one |
| 14 | * of those chunks, e.g. 'ABCD' and would be unable to match strings on |
| 15 | * seams of individual chunks, e.g. 'DEF', because 'D' appears in chunk #1, |
| 16 | * whereas 'EF' in chunk #2. |
| 17 | * |
| 18 | * This class solves this issue by utilizing a sliding window of size 2 chunks. |
| 19 | * That means the callback receives: |
| 20 | * |
| 21 | * ABCDEFGH EFEGHIJKL IJKLMNOP MNOPQRST |
| 22 | * |
| 23 | * That allows for more advanced string manipulation even across chunk seams. |
| 24 | * It is assumed any string searches are much shorter than a chunk size. |
| 25 | * |
| 26 | * @link https://automattic.com |
| 27 | * @since 0.2.0 |
| 28 | * @package automattic/jetpack-boost |
| 29 | */ |
| 30 | |
| 31 | namespace Automattic\Jetpack_Boost\Lib; |
| 32 | |
| 33 | /** |
| 34 | * Class Output_Filter |
| 35 | */ |
| 36 | class Output_Filter { |
| 37 | |
| 38 | /** |
| 39 | * Output chunk size. |
| 40 | */ |
| 41 | const CHUNK_SIZE = 4096; |
| 42 | |
| 43 | /** |
| 44 | * List of callbacks. |
| 45 | * |
| 46 | * @var array |
| 47 | */ |
| 48 | private $callbacks = array(); |
| 49 | |
| 50 | /** |
| 51 | * One chunk always remains in the buffer to allow for cross-seam matching. |
| 52 | * |
| 53 | * @var string |
| 54 | */ |
| 55 | private $buffered_chunk = ''; |
| 56 | |
| 57 | /** |
| 58 | * Whether we allow the callbacks to filter incoming chunks of output. |
| 59 | * |
| 60 | * @var boolean |
| 61 | */ |
| 62 | private $is_filtering = false; |
| 63 | |
| 64 | /** |
| 65 | * Add an output filtering callback. |
| 66 | * |
| 67 | * @param callable $callback Output filtering callback. |
| 68 | * |
| 69 | * @return void |
| 70 | */ |
| 71 | public function add_callback( $callback ) { |
| 72 | $this->callbacks[] = $callback; |
| 73 | |
| 74 | if ( 1 === count( $this->callbacks ) ) { |
| 75 | // Start filtering output now that we have some callbacks. |
| 76 | $this->is_filtering = true; |
| 77 | |
| 78 | ob_start( |
| 79 | array( $this, 'tick' ), |
| 80 | self::CHUNK_SIZE |
| 81 | ); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Processing a full output buffer. |
| 87 | * |
| 88 | * @param string $buffer Output buffer. |
| 89 | * @param int $phase Bitmask of PHP_OUTPUT_HANDLER_* constants. |
| 90 | * |
| 91 | * @return string Buffer data to be flushed to browser. |
| 92 | */ |
| 93 | public function tick( $buffer, $phase ) { |
| 94 | // Bail early if not filtering. |
| 95 | if ( ! $this->is_filtering ) { |
| 96 | return $buffer; |
| 97 | } |
| 98 | |
| 99 | // Check if this the first or last buffer. Use the $phase bitmask to figure it out. |
| 100 | // $phase can contain multiple PHP_OUTPUT_HANDLER_* constants. |
| 101 | // e.g.: PHP_OUTPUT_HANDLER_END = 8 (binary 1000), PHP_OUTPUT_HANDLER_START = 1 (binary 0001). Both = 9 (binary 1001). |
| 102 | // Use bitwise AND to read individual flags from $phase. |
| 103 | $is_first_chunk = ( $phase & PHP_OUTPUT_HANDLER_START ) > 0; |
| 104 | $is_last_chunk = ( $phase & PHP_OUTPUT_HANDLER_END ) > 0; |
| 105 | |
| 106 | // Don't handle the first chunk (unless it's also the last) - we want to output |
| 107 | // one chunk behind the latest to allow for cross-seam matching. |
| 108 | if ( $is_first_chunk && ! $is_last_chunk ) { |
| 109 | $this->buffered_chunk = $buffer; |
| 110 | |
| 111 | return ''; |
| 112 | } |
| 113 | |
| 114 | $buffer_start = $this->buffered_chunk; |
| 115 | $buffer_end = $buffer; |
| 116 | |
| 117 | foreach ( $this->callbacks as $callback ) { |
| 118 | list( $buffer_start, $buffer_end ) = call_user_func( $callback, $buffer_start, $buffer_end ); |
| 119 | } |
| 120 | $this->buffered_chunk = $buffer_end; |
| 121 | $joint_buffer = $buffer_start . $buffer_end; |
| 122 | |
| 123 | // If the second part of the buffer is the last chunk, |
| 124 | // merge the buffer back together to ensure whole output. |
| 125 | if ( $is_last_chunk ) { |
| 126 | // If more buffer chunks arrive, don't apply callbacks to them. |
| 127 | $this->is_filtering = false; |
| 128 | |
| 129 | // Join remaining buffers and allow plugin to append anything to them. |
| 130 | /** |
| 131 | * Filter the Critical CSS output buffer |
| 132 | * |
| 133 | * @param string $joint_buffer The entire output buffer |
| 134 | * @param string $buffer_start The top half of the buffer |
| 135 | * @param string $buffer_end The bottom half of the buffer |
| 136 | * |
| 137 | * @since 1.0.0 |
| 138 | */ |
| 139 | return apply_filters( 'jetpack_boost_output_filtering_last_buffer', $joint_buffer, $buffer_start, $buffer_end ); |
| 140 | } |
| 141 | |
| 142 | // Send the first part of the whole buffer to the browser only, |
| 143 | // because buffer_end will be manipulated in the next tick. |
| 144 | return $buffer_start; |
| 145 | } |
| 146 | } |