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