Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.21% covered (warning)
67.21%
82 / 122
52.94% covered (warning)
52.94%
9 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Modules_Setup
67.21% covered (warning)
67.21%
82 / 122
52.94% covered (warning)
52.94%
9 / 17
186.88
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prime_status_option_caches
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 get_available_modules
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 get_available_submodules
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 get_available_modules_and_submodules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_ready_active_optimization_modules
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get_status
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 register_always_available_endpoints
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 setup_features_data_sync
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 register_endpoints
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 load_modules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_data_sync
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 init_modules
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 setup
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 notice_page_output_change_of_module
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
8.12
 on_module_status_update
57.69% covered (warning)
57.69%
15 / 26
0.00% covered (danger)
0.00%
0 / 1
28.84
 can_module_run
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules;
4
5use Automattic\Jetpack\Schema\Schema;
6use Automattic\Jetpack\WP_JS_Data_Sync\Data_Sync;
7use Automattic\Jetpack_Boost\Admin\Config;
8use Automattic\Jetpack_Boost\Contracts\Changes_Output_After_Activation;
9use Automattic\Jetpack_Boost\Contracts\Feature;
10use Automattic\Jetpack_Boost\Contracts\Has_Data_Sync;
11use Automattic\Jetpack_Boost\Contracts\Has_Setup;
12use Automattic\Jetpack_Boost\Contracts\Needs_Website_To_Be_Public;
13use Automattic\Jetpack_Boost\Data_Sync\Modules_State_Entry;
14use Automattic\Jetpack_Boost\Lib\Setup;
15use Automattic\Jetpack_Boost\Lib\Status;
16use Automattic\Jetpack_Boost\Modules\Optimizations\Cloud_CSS\Cloud_CSS;
17use Automattic\Jetpack_Boost\REST_API\Contracts\Has_Always_Available_Endpoints;
18use Automattic\Jetpack_Boost\REST_API\Contracts\Has_Endpoints;
19use Automattic\Jetpack_Boost\REST_API\REST_API;
20
21class Modules_Setup implements Has_Setup, Has_Data_Sync {
22
23    /**
24     * @var Module[]
25     */
26    protected $available_modules;
27
28    /**
29     * @var Module[]
30     */
31    protected $available_submodules;
32
33    public function __construct() {
34        $this->available_modules    = $this->get_available_modules();
35        $this->available_submodules = $this->get_available_submodules();
36        $this->prime_status_option_caches();
37    }
38
39    /**
40     * Warm the object cache for every module status option in a single query.
41     *
42     * Each module's status is read individually via get_option() when its enabled
43     * state is checked. On a site with no persistent object cache, a module whose
44     * status option has never been stored is re-queried on every request. Priming
45     * the caches here collapses those into one query without creating any rows.
46     */
47    private function prime_status_option_caches() {
48        if ( ! function_exists( 'wp_prime_option_caches' ) ) {
49            return;
50        }
51
52        $option_names = array();
53        foreach ( $this->get_available_modules_and_submodules() as $module ) {
54            $option_names[] = $module->get_status_option_name();
55        }
56
57        if ( ! empty( $option_names ) ) {
58            wp_prime_option_caches( $option_names );
59        }
60    }
61
62    public function get_available_modules() {
63        $available_modules = array();
64        foreach ( Features_Index::FEATURES as $feature ) {
65            $module = new Module( new $feature() );
66            if ( $module->is_available() ) {
67                $available_modules[ $feature::get_slug() ] = $module;
68            }
69        }
70        return $available_modules;
71    }
72
73    public function get_available_submodules() {
74        $available_submodules = array();
75        foreach ( Features_Index::SUB_FEATURES as $feature ) {
76            $module = new Module( new $feature() );
77            if ( $module->is_available() ) {
78                $available_submodules[ $feature::get_slug() ] = $module;
79            }
80        }
81        return $available_submodules;
82    }
83
84    public function get_available_modules_and_submodules() {
85        return array_merge( $this->available_modules, $this->available_submodules );
86    }
87
88    /**
89     * Get modules that are currently active and optimizing the site.
90     *
91     * @return string[] Slugs of optimization modules that are currently active and serving.
92     */
93    public function get_ready_active_optimization_modules() {
94        $working_modules = array();
95        foreach ( $this->get_available_modules_and_submodules() as $slug => $module ) {
96            if ( $module->is_optimizing() ) {
97                $working_modules[] = $slug;
98            }
99        }
100        return $working_modules;
101    }
102
103    /**
104     * Get the status of all available modules and submodules.
105     *
106     * @return array<string, bool> Slugs of modules and their status.
107     */
108    public function get_status() {
109        $status = array();
110        foreach ( $this->get_available_modules_and_submodules() as $slug => $module ) {
111            $status[ $slug ] = $module->is_enabled();
112        }
113        return $status;
114    }
115
116    /**
117     * Used to register endpoints that will be available even
118     * if the module is not enabled.
119     *
120     * @return bool|void
121     */
122    public function register_always_available_endpoints() {
123        foreach ( Features_Index::get_all_features() as $feature_class ) {
124            $feature = new $feature_class();
125
126            if ( ! $feature instanceof Has_Always_Available_Endpoints || ! $feature instanceof Feature ) {
127                continue;
128            }
129
130            if ( empty( $feature->get_always_available_endpoints() ) ) {
131                return false;
132            }
133
134            $module = new Module( $feature );
135            if ( ! $module->is_available() ) {
136                continue;
137            }
138
139            REST_API::register( $feature->get_always_available_endpoints() );
140        }
141    }
142
143    private function setup_features_data_sync() {
144        foreach ( Features_Index::get_all_features() as $feature_class ) {
145            $feature = new $feature_class();
146            if ( ! $feature instanceof Has_Data_Sync ) {
147                continue;
148            }
149
150            $feature->register_data_sync( Data_Sync::get_instance( JETPACK_BOOST_DATASYNC_NAMESPACE ) );
151        }
152    }
153
154    private function register_endpoints( $feature ) {
155        if ( ! $feature instanceof Has_Endpoints ) {
156            return false;
157        }
158
159        if ( empty( $feature->get_endpoints() ) ) {
160            return false;
161        }
162
163        REST_API::register( $feature->get_endpoints() );
164    }
165
166    public function load_modules() {
167        $this->init_modules( $this->available_modules );
168    }
169
170    /**
171     * Registers general data sync for the modules.
172     */
173    public function register_data_sync( $instance ) {
174        $modules_state_schema = Schema::as_array(
175            Schema::as_assoc_array(
176                array(
177                    'active'    => Schema::as_boolean()->fallback( false ),
178                    'available' => Schema::as_boolean()->nullable(),
179                )
180            )
181        )->fallback( array() );
182
183        $entry = new Modules_State_Entry( array_merge( Features_Index::FEATURES, Features_Index::SUB_FEATURES ) );
184        $instance->register( 'modules_state', $modules_state_schema, $entry );
185    }
186
187    /**
188     * Initialize the modules.
189     *
190     * @param Module[] $modules The modules to initialize.
191     */
192    private function init_modules( array $modules ) {
193        foreach ( $modules as $slug => $module ) {
194            if ( ! $module->is_enabled() ) {
195                continue;
196            }
197
198            if ( ! $this->can_module_run( $module ) ) {
199                continue;
200            }
201
202            Setup::add( $module->feature );
203
204            $submodules = $module->get_available_submodules();
205            if ( ! empty( $submodules ) ) {
206                $this->init_modules( $submodules );
207            }
208
209            $this->register_endpoints( $module->feature );
210
211            do_action( "jetpack_boost_{$slug}_initialized", $this );
212        }
213    }
214
215    /**
216     * @inheritDoc
217     */
218    public function setup() {
219        // We need to setup data sync outside of plugins_loaded to prevent side effects on other classes that are loaded from other actions earlier.
220        self::register_data_sync( Data_Sync::get_instance( JETPACK_BOOST_DATASYNC_NAMESPACE ) );
221        $this->setup_features_data_sync();
222        $this->register_always_available_endpoints();
223        add_action( 'plugins_loaded', array( $this, 'load_modules' ) );
224        add_action( 'jetpack_boost_module_status_updated', array( $this, 'on_module_status_update' ), 10, 2 );
225
226        // Add a hook to fire page output changed action when a module that Changes_Output_After_Activation indicates something has changed.
227        foreach ( $this->get_available_modules_and_submodules() as $module ) {
228            $this->notice_page_output_change_of_module( $module );
229        }
230    }
231
232    private function notice_page_output_change_of_module( $module ) {
233        if ( ! $module->is_enabled() ) {
234            return;
235        }
236
237        $feature = $module->feature;
238        if ( ! ( $feature instanceof Changes_Output_After_Activation ) ) {
239            return;
240        }
241
242        $action_names = $feature::get_change_output_action_names();
243        if ( empty( $action_names ) ) {
244            return;
245        }
246
247        foreach ( $action_names as $action ) {
248            add_action( $action, array( $module, 'indicate_page_output_changed' ), 10, 1 );
249        }
250    }
251
252    /**
253     * Handle module status changes.
254     *
255     * @param string $module_slug The module slug.
256     * @param bool   $is_activated The new status.
257     */
258    public function on_module_status_update( $module_slug, $is_activated ) {
259        $modules = $this->get_available_modules_and_submodules();
260        if ( Cloud_CSS::get_slug() === $module_slug && ! isset( $modules[ $module_slug ] ) ) {
261            // A plan refresh can make Cloud CSS available after this list was built.
262            $this->available_modules = $this->get_available_modules();
263            $modules                 = $this->get_available_modules_and_submodules();
264        }
265
266        if ( ! isset( $modules[ $module_slug ] ) ) {
267            return;
268        }
269
270        $module = $modules[ $module_slug ];
271
272        if ( ! $module ) {
273            return;
274        }
275
276        $status = new Status( $module_slug );
277        $status->on_update( $is_activated );
278
279        if ( ! $this->can_module_run( $module ) ) {
280            return;
281        }
282
283        if ( $is_activated ) {
284            $module->on_activate();
285        } else {
286            $module->on_deactivate();
287        }
288
289        // Now run the activation/deactivation for all submodules that are effected by this modules status change.
290        $submodules = $module->get_available_submodules();
291        if ( is_array( $submodules ) ) {
292            foreach ( $submodules as $submodule ) {
293                // Only worry about submodules that are enabled.
294                if ( ! $submodule->is_enabled() ) {
295                    continue;
296                }
297
298                $active_parent_modules = $submodule->get_active_parent_modules();
299
300                if ( $is_activated && count( $active_parent_modules ) === 1 ) {
301                    // If current module is the only active parent module, run activation on the submodule.
302                    // If this submodule has other parent modules, we can assume they are already activated.
303                    $submodule->on_activate();
304                }
305
306                // If submodule has no active parent modules left, run deactivate on the submodule.
307                // If this submodule still has other parent modules, we can assume they are not ready to be deactivated.
308                if ( ! $is_activated && empty( $active_parent_modules ) ) {
309                    $submodule->on_deactivate();
310                }
311            }
312        }
313    }
314
315    /**
316     * Determines whether the functionality of a module should run.
317     * If the website is not public but the module requires it,
318     * the module's functionality should not run.
319     *
320     * @param Module $module The module to check.
321     * @return bool True if the module can run, false otherwise.
322     */
323    public function can_module_run( $module ) {
324        $website_public = Config::is_website_public();
325        $can_module_run = true;
326
327        // If the module requires the website to be public and it's not, don't allow it to run.
328        if ( $module->feature instanceof Needs_Website_To_Be_Public && ! $website_public ) {
329            $can_module_run = false;
330        }
331
332        /**
333         * Filter to allow modules to run even if the website is not public.
334         * This is useful for debugging purposes.
335         *
336         * @since 4.2.0
337         *
338         * @param bool   $can_module_run Whether the module should be disabled.
339         * @param Module $module         The module to check.
340         * @param bool   $website_public Whether the website is public.
341         */
342        return apply_filters( 'jetpack_boost_can_module_run', $can_module_run, $module, $website_public );
343    }
344}