Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Output_Filter
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 2
72
0.00% covered (danger)
0.00%
0 / 1
 add_callback
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 tick
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
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
31namespace Automattic\Jetpack_Boost\Lib;
32
33/**
34 * Class Output_Filter
35 */
36class 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}