Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.54% covered (success)
90.54%
67 / 74
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Module_Control
93.06% covered (success)
93.06%
67 / 72
85.71% covered (warning)
85.71%
12 / 14
43.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
9.66
 is_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_instant_search_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_swap_classic_to_inline_search
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 activate
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 deactivate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 update_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 disable_instant_search
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 enable_instant_search
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 update_instant_search_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 update_swap_classic_to_inline_search
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_experience
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 update_experience
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
10
 get_active_modules
n/a
0 / 0
n/a
0 / 0
1
 search_filter_available_modules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jetpack Search: Module_Control class
4 *
5 * @package automattic/jetpack-search
6 */
7
8namespace Automattic\Jetpack\Search;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Modules;
12use Automattic\Jetpack\Status;
13use WP_Error;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19/**
20 * To get and set Search module settings
21 */
22class Module_Control {
23    /**
24     * Plan object
25     *
26     * @var Plan
27     */
28    protected $plan;
29
30    /**
31     * Connection_Manager object
32     *
33     * @var \Automattic\Jetpack\Connection\Manager
34     */
35    protected $connection_manager;
36
37    /**
38     * We use the same options as Jetpack the plugin to flag whether Search is active.
39     */
40    const JETPACK_ACTIVE_MODULES_OPTION_KEY               = 'active_modules';
41    const JETPACK_SEARCH_MODULE_SLUG                      = 'search';
42    const SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY         = 'instant_search_enabled';
43    const SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY = 'swap_classic_to_inline_search';
44    const SEARCH_MODULE_EXPERIENCE_OPTION_KEY             = 'jetpack_search_experience';
45
46    /**
47     * Valid experience values.
48     */
49    const EXPERIENCE_OVERLAY  = 'overlay';
50    const EXPERIENCE_EMBEDDED = 'embedded';
51    const EXPERIENCE_INLINE   = 'inline';
52    const EXPERIENCE_OFF      = 'off';
53
54    /**
55     * Contructor
56     *
57     * @param Plan|null                                   $plan - Plan object.
58     * @param \Automattic\Jetpack\Connection\Manager|null $connection_manager - Connection_Manager object.
59     */
60    public function __construct( $plan = null, $connection_manager = null ) {
61        $this->plan               = $plan === null ? new Plan() : $plan;
62        $this->connection_manager = $connection_manager === null ? new Connection_Manager( Package::SLUG ) : $connection_manager;
63        if ( ! did_action( 'jetpack_search_module_control_initialized' ) ) {
64            add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'search_filter_available_modules' ), 10, 1 );
65            if ( Helper::is_wpcom() ) {
66                add_filter( 'jetpack_active_modules', array( $this, 'search_filter_available_modules' ), 10, 2 );
67            }
68            /**
69             * Fires when the Automattic\Jetpack\Search\Module_Control is initialized for the first time.
70             */
71            do_action( 'jetpack_search_module_control_initialized' );
72        }
73    }
74
75    /**
76     * Returns a boolean for whether of the module is enabled.
77     *
78     * @return bool
79     */
80    public function is_active() {
81        return ( new Modules() )->is_active( self::JETPACK_SEARCH_MODULE_SLUG );
82    }
83
84    /**
85     * Returns a boolean for whether instant search is enabled.
86     *
87     * @return bool
88     */
89    public function is_instant_search_enabled() {
90        return (bool) $this->plan->supports_instant_search() && get_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY );
91    }
92
93    /**
94     * Returns a boolean for whether new inline search is enabled.
95     *
96     * @return bool
97     */
98    public function is_swap_classic_to_inline_search() {
99        return (bool) get_option( self::SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY, false );
100    }
101
102    /**
103     * Activiate Search module
104     */
105    public function activate() {
106        $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
107        if ( ( new Status() )->is_offline_mode() ) {
108            return new WP_Error( 'site_offline', __( 'Jetpack Search cannot be used in offline mode.', 'jetpack-search-pkg' ) );
109        }
110        if ( ! $is_wpcom && ! $this->connection_manager->is_connected() ) {
111            return new WP_Error( 'connection_required', __( 'Connect your site to use Jetpack Search.', 'jetpack-search-pkg' ) );
112        }
113        if ( ! $this->plan->supports_search() ) {
114            return new WP_Error( 'not_supported', __( 'Your plan does not support Jetpack Search.', 'jetpack-search-pkg' ) );
115        }
116
117        $success = ( new Modules() )->activate( self::JETPACK_SEARCH_MODULE_SLUG, false, false );
118        if ( false === $success ) {
119            return new WP_Error( 'not_updated', __( 'Setting not updated.', 'jetpack-search-pkg' ) );
120        }
121        return $success;
122    }
123
124    /**
125     * Deactiviate Search module
126     */
127    public function deactivate() {
128        $success = ( new Modules() )->deactivate( self::JETPACK_SEARCH_MODULE_SLUG );
129
130        $this->disable_instant_search();
131
132        return $success;
133    }
134
135    /**
136     * Update module status
137     *
138     * @param boolean $active - true to activate, false to deactivate.
139     */
140    public function update_status( $active ) {
141        return $active ? $this->activate() : $this->deactivate();
142    }
143
144    /**
145     * Disable Instant Search Experience
146     */
147    public function disable_instant_search() {
148        return update_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY, false );
149    }
150
151    /**
152     * Enable Instant Search Experience
153     */
154    public function enable_instant_search() {
155        if ( ! $this->is_active() ) {
156            return new WP_Error( 'search_module_inactive', __( 'Search module needs to be activated before enabling instant search.', 'jetpack-search-pkg' ) );
157        }
158        if ( ! $this->plan->supports_instant_search() ) {
159            return new WP_Error( 'not_supported', __( 'Your plan does not support Instant Search.', 'jetpack-search-pkg' ) );
160        }
161        return update_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY, true );
162    }
163
164    /**
165     * Update instant search status
166     *
167     * @param boolean $enabled - true to enable, false to disable.
168     */
169    public function update_instant_search_status( $enabled ) {
170        return $enabled ? $this->enable_instant_search() : $this->disable_instant_search();
171    }
172
173    /**
174     * Update setting indicating whether inline search should use newer 1.3 API.
175     *
176     * @param bool $swap_classic_to_inline_search - true to use Inline Search, false to use Classic Search.
177     */
178    public function update_swap_classic_to_inline_search( bool $swap_classic_to_inline_search ) {
179        return update_option( self::SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY, $swap_classic_to_inline_search );
180    }
181
182    /**
183     * Get the active search experience.
184     *
185     * The wire format always resolves to one of the four values, but storage is narrower:
186     * `'off'` is read from the global Jetpack module-active state (not stored in this
187     * package's option), and `'inline'` is the absence of an opt-in (the option is
188     * deleted, not written as `'inline'`). Only `'embedded'` and `'overlay'` are
189     * actually written to `jetpack_search_experience`.
190     *
191     * @return string One of 'embedded', 'overlay', 'inline', 'off'.
192     */
193    public function get_experience() {
194        if ( ! $this->is_active() ) {
195            return self::EXPERIENCE_OFF;
196        }
197
198        $saved = get_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, false );
199        if ( self::EXPERIENCE_EMBEDDED === $saved ) {
200            return self::EXPERIENCE_EMBEDDED;
201        }
202        if ( self::EXPERIENCE_OVERLAY === $saved ) {
203            return self::EXPERIENCE_OVERLAY;
204        }
205
206        // Legacy fallback for sites that have never saved via the new UI: a true
207        // `instant_search_enabled` boolean reads as overlay; otherwise inline.
208        if ( $this->is_instant_search_enabled() ) {
209            return self::EXPERIENCE_OVERLAY;
210        }
211
212        return self::EXPERIENCE_INLINE;
213    }
214
215    /**
216     * Update the search experience.
217     *
218     * Storage is narrower than the wire format: `'off'` only deactivates the global
219     * module (no write to the experience option), and `'inline'` deletes the
220     * experience option (the absence of an opt-in *is* inline). Only `'embedded'`
221     * and `'overlay'` write affirmative values.
222     *
223     * Legacy `module_active` / `instant_search_enabled` are kept in lockstep so
224     * unmigrated readers (Initializer, Options, sidebar registration) continue to
225     * see the right state until they're migrated to consult get_experience().
226     *
227     * @param string $experience One of 'embedded', 'overlay', 'inline', 'off'.
228     * @return bool|WP_Error WP_Error on failure; true on success for the affirmative
229     *                      branches; the bool from Modules::deactivate() for `'off'`
230     *                      (false signals the module was already inactive — a benign
231     *                      no-op the REST controller treats as success).
232     */
233    public function update_experience( string $experience ) {
234        $valid_values = array( self::EXPERIENCE_OVERLAY, self::EXPERIENCE_EMBEDDED, self::EXPERIENCE_INLINE, self::EXPERIENCE_OFF );
235        if ( ! in_array( $experience, $valid_values, true ) ) {
236            return new WP_Error(
237                'invalid_experience',
238                esc_html__( 'Invalid experience value.', 'jetpack-search-pkg' ),
239                array( 'status' => 400 )
240            );
241        }
242
243        switch ( $experience ) {
244            case self::EXPERIENCE_OFF:
245                // Off lives in the global jetpack_active_modules option, not in this
246                // package's experience option. Leave instant_search_enabled and the
247                // experience option untouched so re-enabling later restores the user's
248                // prior preference (matches legacy ModuleControl behaviour).
249                return ( new Modules() )->deactivate( self::JETPACK_SEARCH_MODULE_SLUG );
250
251            case self::EXPERIENCE_INLINE:
252                $result = $this->activate();
253                if ( is_wp_error( $result ) ) {
254                    return $result;
255                }
256                $this->disable_instant_search();
257                // Inline is the absence of an opt-in — delete the option rather than
258                // writing 'inline'. Pre-existing sites that have never saved are
259                // already in this state, so this also normalises after a switch.
260                delete_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY );
261                return true;
262
263            case self::EXPERIENCE_EMBEDDED:
264                $result = $this->activate();
265                if ( is_wp_error( $result ) ) {
266                    return $result;
267                }
268                $this->disable_instant_search();
269                update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_EMBEDDED );
270                return true;
271
272            case self::EXPERIENCE_OVERLAY:
273                $result = $this->activate();
274                if ( is_wp_error( $result ) ) {
275                    return $result;
276                }
277                $result = $this->enable_instant_search();
278                if ( is_wp_error( $result ) ) {
279                    return $result;
280                }
281                update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_OVERLAY );
282                return true;
283        }
284    }
285
286    /**
287     * Get a list of activated modules as an array of module slugs.
288     *
289     * @deprecated 0.12.3
290     * @return Array $active_modules
291     */
292    public function get_active_modules() {
293        _deprecated_function(
294            __METHOD__,
295            'jetpack-search-0.12.3',
296            'Automattic\\Jetpack\\Modules\\get_active'
297        );
298
299        return ( new Modules() )->get_active();
300    }
301
302    /**
303     * Adds search to the list of available modules
304     *
305     * @param array $modules The available modules.
306     * @return array
307     */
308    public function search_filter_available_modules( $modules ) {
309        return array_merge( array( self::JETPACK_SEARCH_MODULE_SLUG ), $modules );
310    }
311}