Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.82% covered (warning)
58.82%
40 / 68
37.50% covered (danger)
37.50%
6 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Module
58.82% covered (warning)
58.82%
40 / 68
37.50% covered (danger)
37.50%
6 / 16
201.22
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
 on_activate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 on_deactivate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 indicate_page_output_changed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_status_option_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_submodules
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_available_submodules
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 is_disabled_dev_feature
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_active_parent_modules
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 update
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_always_on
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_available
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
7.10
 is_force_disabled
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 is_optimizing
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
9.30
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules;
4
5use Automattic\Jetpack\Boost\App\Contracts\Is_Dev_Feature;
6use Automattic\Jetpack_Boost\Contracts\Changes_Output_After_Activation;
7use Automattic\Jetpack_Boost\Contracts\Changes_Output_On_Activation;
8use Automattic\Jetpack_Boost\Contracts\Feature;
9use Automattic\Jetpack_Boost\Contracts\Has_Activate;
10use Automattic\Jetpack_Boost\Contracts\Has_Deactivate;
11use Automattic\Jetpack_Boost\Contracts\Needs_To_Be_Ready;
12use Automattic\Jetpack_Boost\Contracts\Optimization;
13use Automattic\Jetpack_Boost\Contracts\Sub_Feature;
14use Automattic\Jetpack_Boost\Lib\Status;
15
16class Module {
17    const DISABLE_MODULE_QUERY_VAR = 'jb-disable-modules';
18
19    /**
20     * @var Status
21     */
22    private $status;
23
24    /**
25     * @var Feature
26     */
27    public $feature;
28
29    public function __construct( Feature $feature ) {
30        $this->feature = $feature;
31        $this->status  = new Status( $feature::get_slug() );
32    }
33
34    public function on_activate() {
35        if ( $this->feature instanceof Changes_Output_On_Activation ) {
36            $this->indicate_page_output_changed();
37        }
38
39        return $this->feature instanceof Has_Activate ? $this->feature::activate() : true;
40    }
41
42    public function on_deactivate() {
43        // If the module changes the page output, with or without preparation, deactivating the module should indicate a page output change.
44        if ( $this->feature instanceof Changes_Output_On_Activation || $this->feature instanceof Changes_Output_After_Activation ) {
45            $this->indicate_page_output_changed();
46        }
47
48        return $this->feature instanceof Has_Deactivate ? $this->feature::deactivate() : true;
49    }
50
51    public function indicate_page_output_changed() {
52        /**
53         * Indicate that the HTML output of front-end has changed.
54         *
55         * If there is any page cache, it should be invalidated when this action is triggered.
56         */
57        do_action( 'jetpack_boost_page_output_changed' );
58    }
59
60    public function get_slug() {
61        return $this->feature::get_slug();
62    }
63
64    /**
65     * Name of the wp_options option that stores this module's status.
66     *
67     * @return string
68     */
69    public function get_status_option_name() {
70        return Status::get_option_name( $this->get_slug() );
71    }
72
73    /**
74     * If the module has any submodules, this method will return an array of Module instances for each submodule.
75     */
76    public function get_submodules() {
77        $subfeatures = Features_Index::get_sub_features_of( $this->feature );
78
79        $modules = array();
80        foreach ( $subfeatures as $subfeature ) {
81            $modules[ $subfeature::get_slug() ] = new Module( new $subfeature() );
82        }
83
84        return $modules;
85    }
86
87    public function get_available_submodules() {
88        $submodules = $this->get_submodules();
89        if ( empty( $submodules ) ) {
90            return array();
91        }
92
93        $available_submodules = array();
94        foreach ( $submodules as $slug => $submodule ) {
95            if ( $submodule->is_available() && ! $this->is_disabled_dev_feature( $submodule->feature ) ) {
96                $available_submodules[ $slug ] = $submodule;
97            }
98        }
99
100        return $available_submodules;
101    }
102
103    /**
104     * Check if the feature is disabled in development.
105     *
106     * Returns true if the feature is a dev feature and the dev features should be disabled.
107     *
108     * @param Feature $feature The feature to check.
109     * @return bool True if the feature is available, false otherwise.
110     */
111    private function is_disabled_dev_feature( $feature ) {
112        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
113        $is_disabled_dev_feature = false === strpos( $_SERVER['HTTP_HOST'] ?? '', 'jurassic.ninja' );
114        if ( defined( 'JETPACK_BOOST_DEVELOPMENT_FEATURES' ) ) {
115            $is_disabled_dev_feature = ! JETPACK_BOOST_DEVELOPMENT_FEATURES;
116        }
117
118        if ( $feature instanceof Is_Dev_Feature ) {
119            return $is_disabled_dev_feature;
120        }
121
122        return false;
123    }
124
125    /**
126     * Get the active parent modules.
127     *
128     * @return Module[] The active parent modules.
129     */
130    public function get_active_parent_modules() {
131        if ( ! $this->feature instanceof Sub_Feature ) {
132            return array();
133        }
134
135        $parent_features = $this->feature->get_parent_features();
136        $modules         = array();
137        foreach ( $parent_features as $parent_feature ) {
138            $parent_module = new Module( new $parent_feature() );
139            if ( $parent_module->is_enabled() ) {
140                $modules[ $parent_module->get_slug() ] = $parent_module;
141            }
142        }
143
144        return $modules;
145    }
146
147    public function update( $new_status ) {
148        return $this->status->set( $new_status );
149    }
150
151    /**
152     * Check if the module is enabled.
153     *
154     * If the module is always on, it is enabled. Otherwise, check database for the module status.
155     * If it's a submodule, the status is only about the submodule itself, not its parent modules.
156     *
157     * @return bool True if the module is enabled, false otherwise.
158     */
159    public function is_enabled() {
160        if ( $this->is_always_on() ) {
161            return true;
162        }
163
164        return $this->status->get();
165    }
166
167    /**
168     * Check if the module's feature implements Is_Always_On.
169     *
170     * Always-on modules cannot be disabled: is_enabled() short-circuits to true
171     * for them regardless of the persisted option, so writes via update() are
172     * silently overridden. Callers writing module state should bail before the
173     * write rather than letting on-disk state diverge from runtime state.
174     *
175     * @return bool
176     */
177    public function is_always_on(): bool {
178        return is_subclass_of( $this->feature, 'Automattic\Jetpack_Boost\Contracts\Is_Always_On' );
179    }
180
181    /**
182     * Check if the module is available.
183     *
184     * If the module is not available, it cannot be enabled.
185     */
186    public function is_available() {
187        if ( ! $this->feature::is_available() || $this->is_disabled_dev_feature( $this->feature ) || $this->is_force_disabled() ) {
188            return false;
189        }
190
191        // If the module is not a sub-module, and it already passed the availability check, it is available.
192        if ( ! $this->feature instanceof Sub_Feature ) {
193            return true;
194        }
195
196        // If the module is a sub-module, it is available if at least one of its parent modules is available.
197        foreach ( $this->feature::get_parent_features() as $parent_feature ) {
198            if ( ( new Module( new $parent_feature() ) )->is_available() ) {
199                return true;
200            }
201        }
202
203        return false;
204    }
205
206    private function is_force_disabled() {
207        $slug = $this->feature::get_slug();
208
209        // phpcs:disable WordPress.Security.NonceVerification.Recommended
210        if ( ! empty( $_GET[ self::DISABLE_MODULE_QUERY_VAR ] ) ) {
211            // phpcs:disable WordPress.Security.NonceVerification.Recommended
212            // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
213            // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
214            $disabled_modules = array_map( 'sanitize_key', explode( ',', $_GET[ self::DISABLE_MODULE_QUERY_VAR ] ) );
215            return in_array( $slug, $disabled_modules, true ) || in_array( 'all', $disabled_modules, true );
216        }
217
218        return false;
219    }
220
221    /**
222     * Check if the module is active and ready to serve optimized output.
223     */
224    public function is_optimizing() {
225        if ( ! $this->is_available() ) {
226            return false;
227        }
228
229        if ( ! ( $this->feature instanceof Optimization ) || ! $this->is_enabled() ) {
230            return false;
231        }
232
233        if ( $this->feature instanceof Needs_To_Be_Ready && ! $this->feature->is_ready() ) {
234            return false;
235        }
236
237        if ( $this->feature instanceof Sub_Feature ) {
238            $parent_modules = $this->get_active_parent_modules();
239            if ( empty( $parent_modules ) ) {
240                return false;
241            }
242        }
243
244        return true;
245    }
246}