Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
54 / 60
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Waf_Standalone_Bootstrap
90.00% covered (success)
90.00%
54 / 60
75.00% covered (warning)
75.00%
6 / 8
25.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 guard_against_missing_abspath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 initialize_constants
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initialize_filesystem
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 locate_classmap_file
75.00% covered (warning)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
10.27
 get_bootstrap_file_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_entrypoint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 generate
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2/**
3 * Handles generation and deletion of the bootstrap for the standalone WAF mode.
4 *
5 * @package automattic/jetpack-waf
6 */
7
8namespace Automattic\Jetpack\Waf;
9
10use Composer\InstalledVersions;
11
12/**
13 * Handles the bootstrap.
14 *
15 * @phan-constructor-used-for-side-effects
16 */
17class Waf_Standalone_Bootstrap {
18
19    /**
20     * Ensures that constants are initialized if this class is used.
21     *
22     * @return void
23     */
24    public function __construct() {
25        $this->guard_against_missing_abspath();
26        $this->initialize_constants();
27    }
28
29    /**
30     * Ensures that this class is not used unless we are in the right context.
31     *
32     * @throws Waf_Exception If we are outside of WordPress.
33     *
34     * @return void
35     */
36    private function guard_against_missing_abspath() {
37
38        if ( ! defined( 'ABSPATH' ) ) {
39            throw new Waf_Exception( 'Cannot generate the WAF bootstrap if we are not running in WordPress context.' );
40        }
41    }
42
43    /**
44     * Initializes the constants required for generating the bootstrap, if they have not been initialized yet.
45     *
46     * @return void
47     */
48    private function initialize_constants() {
49        Waf_Constants::initialize_constants();
50    }
51
52    /**
53     * Initialized the WP filesystem and serves as a mocking hook for tests.
54     *
55     * Should only be implemented after the wp_loaded action hook:
56     *
57     * @link https://developer.wordpress.org/reference/functions/wp_filesystem/#more-information
58     *
59     * @return void
60     */
61    protected function initialize_filesystem() {
62        if ( ! function_exists( '\\WP_Filesystem' ) ) {
63            require_once ABSPATH . 'wp-admin/includes/file.php';
64        }
65
66        WP_Filesystem();
67    }
68
69    /**
70     * Finds the path to Composer's generated classmap, which the generated bootstrap file uses to autoload WAF classes.
71     *
72     * @throws Waf_Exception In case the classmap file cannot be found.
73     *
74     * @return string
75     */
76    private function locate_classmap_file() {
77        global $jetpack_autoloader_loader;
78
79        $vendor_dirs = array();
80
81        // Try the Jetpack autoloader.
82        if ( isset( $jetpack_autoloader_loader ) ) {
83            $class_file = $jetpack_autoloader_loader->find_class_file( Waf_Runner::class );
84            if ( $class_file ) {
85                $vendor_dirs[] = dirname( $class_file, 5 ) . '/vendor';
86            }
87        }
88
89        // Try Composer's autoloader.
90        if ( is_callable( array( InstalledVersions::class, 'getInstallPath' ) )
91            && InstalledVersions::isInstalled( 'automattic/jetpack-waf' )
92        ) {
93            $package_file = InstalledVersions::getInstallPath( 'automattic/jetpack-waf' );
94            if ( substr( $package_file, -23 ) === '/automattic/jetpack-waf' ) {
95                $vendor_dirs[] = dirname( $package_file, 3 ) . '/vendor';
96            }
97        }
98
99        // Guess. First look for being in a `vendor/automattic/jetpack-waf/src/', then see if we're standalone with our own vendor dir.
100        $vendor_dirs[] = dirname( __DIR__, 4 ) . '/vendor';
101        $vendor_dirs[] = dirname( __DIR__ ) . '/vendor';
102
103        // A candidate can exist without containing this package, e.g. a monorepo's root vendor dir.
104        foreach ( $vendor_dirs as $vendor_dir ) {
105            $classmap_file = $vendor_dir . '/composer/autoload_classmap.php';
106            if ( ! file_exists( $classmap_file ) ) {
107                continue;
108            }
109            $classmap = require $classmap_file;
110            if ( isset( $classmap[ Waf_Runner::class ] ) ) {
111                return $classmap_file;
112            }
113        }
114
115        throw new Waf_Exception( 'Cannot find the Composer classmap, and the WAF standalone bootstrap will not work without it.' );
116    }
117
118    /**
119     * Gets the path to the bootstrap.php file.
120     *
121     * @return string The bootstrap.php file path.
122     */
123    public function get_bootstrap_file_path() {
124        return trailingslashit( JETPACK_WAF_DIR ) . 'bootstrap.php';
125    }
126
127    /**
128     * Gets the entrypoint file.
129     *
130     * @return string The entrypoint file.
131     */
132    private function get_entrypoint() {
133        return defined( 'JETPACK_WAF_ENTRYPOINT' ) ? JETPACK_WAF_ENTRYPOINT : 'rules/rules.php';
134    }
135
136    /**
137     * Generates the bootstrap file.
138     *
139     * @throws File_System_Exception If the filesystem is not available.
140     * @throws File_System_Exception If the WAF directory cannot be created.
141     * @throws File_System_Exception If the bootstrap file cannot be created.
142     *
143     * @return string Absolute path to the bootstrap file.
144     */
145    public function generate() {
146
147        $this->initialize_filesystem();
148
149        global $wp_filesystem;
150        if ( ! $wp_filesystem ) {
151            throw new File_System_Exception( 'Cannot work without the file system being initialized.' );
152        }
153
154        $classmap_file = $this->locate_classmap_file();
155
156        $bootstrap_file          = $this->get_bootstrap_file_path();
157        $entrypoint              = $this->get_entrypoint();
158        $mode_option             = get_option( Waf_Runner::MODE_OPTION_NAME, false );
159        $share_data_option       = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
160        $share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
161
162        // Autoload from the classmap alone rather than `vendor/autoload.php`: Composer's loader would also run every
163        // package's `files` entries and mark them loaded, so the Jetpack autoloader later skips its own, possibly newer, copies.
164        // The closure keeps every variable, including the classmap's own `$vendorDir`/`$baseDir`, out of the global scope.
165        $template = <<<'PHP'
166        <?php
167        define( 'DISABLE_JETPACK_WAF', {{disable}} );
168        if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;
169        define( 'JETPACK_WAF_MODE', {{mode}} );
170        define( 'JETPACK_WAF_SHARE_DATA', {{share_data}} );
171        define( 'JETPACK_WAF_SHARE_DEBUG_DATA', {{share_debug_data}} );
172        define( 'JETPACK_WAF_DIR', {{dir}} );
173        define( 'JETPACK_WAF_WPCONFIG', {{wpconfig}} );
174        define( 'JETPACK_WAF_ENTRYPOINT', {{entrypoint}} );
175        ( static function () {
176            $classmap_file = {{classmap_file}};
177            if ( ! is_file( $classmap_file ) ) {
178                return;
179            }
180            $classmap   = require $classmap_file;
181            $autoloader = static function ( $class_name ) use ( $classmap ) {
182                if ( isset( $classmap[ $class_name ] ) ) {
183                    require $classmap[ $class_name ];
184                }
185            };
186            spl_autoload_register( $autoloader );
187            Automattic\Jetpack\Waf\Waf_Runner::initialize();
188            spl_autoload_unregister( $autoloader );
189
190            // The preloaded WAF classes keep running once WordPress starts, and the active plugin's older copy may lack a class
191            // they reference. Keep resolving WAF classes from here, behind the Jetpack autoloader, which prepends itself.
192            $waf_classmap = array_filter(
193                $classmap,
194                static function ( $class_name ) {
195                    return 0 === strpos( $class_name, 'Automattic\Jetpack\Waf\\' );
196                },
197                ARRAY_FILTER_USE_KEY
198            );
199            spl_autoload_register(
200                static function ( $class_name ) use ( $waf_classmap ) {
201                    if ( isset( $waf_classmap[ $class_name ] ) ) {
202                        require $waf_classmap[ $class_name ];
203                    }
204                }
205            );
206        } )();
207
208        PHP;
209
210        $code = strtr(
211            $template,
212            array(
213                '{{disable}}'          => var_export( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF, true ),
214                '{{mode}}'             => var_export( $mode_option ? $mode_option : 'silent', true ),
215                '{{share_data}}'       => var_export( $share_data_option, true ),
216                '{{share_debug_data}}' => var_export( $share_debug_data_option, true ),
217                '{{dir}}'              => var_export( JETPACK_WAF_DIR, true ),
218                '{{wpconfig}}'         => var_export( JETPACK_WAF_WPCONFIG, true ),
219                '{{entrypoint}}'       => var_export( $entrypoint, true ),
220                '{{classmap_file}}'    => var_export( $classmap_file, true ),
221            )
222        );
223
224        if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
225            if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
226                throw new File_System_Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR );
227            }
228        }
229
230        if ( ! $wp_filesystem->put_contents( $bootstrap_file, $code ) ) {
231            throw new File_System_Exception( 'Failed writing WAF standalone bootstrap file to: ' . $bootstrap_file );
232        }
233
234        return $bootstrap_file;
235    }
236}