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