PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-search/src/class-module-control.php +421 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,421 @@
1 +<?php
2 +/**
3 + * Jetpack Search: Module_Control class
4 + *
5 + * @package automattic/jetpack-search
6 + */
7 +
8 +namespace Automattic\Jetpack\Search;
9 +
10 +use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11 +use Automattic\Jetpack\Modules;
12 +use Automattic\Jetpack\Status;
13 +use WP_Error;
14 +
15 +if ( ! defined( 'ABSPATH' ) ) {
16 + exit( 0 );
17 +}
18 +
19 +/**
20 + * To get and set Search module settings
21 + */
22 +class 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 + * `EXPERIENCE_OVERLAY_BLOCKS` is the experimental blocks-powered overlay
50 + * (server-rendered Search blocks template inside a modal). It is gated
51 + * by the `jetpack_search_overlay_block_template_enabled` filter — the
52 + * dashboard option only appears when the filter is on, and the runtime
53 + * swap from the legacy preact app to the new overlay only kicks in when
54 + * the user has explicitly chosen this experience.
55 + */
56 + const EXPERIENCE_OVERLAY = 'overlay';
57 + const EXPERIENCE_OVERLAY_BLOCKS = 'overlay_blocks';
58 + const EXPERIENCE_EMBEDDED = 'embedded';
59 + const EXPERIENCE_INLINE = 'inline';
60 + const EXPERIENCE_OFF = 'off';
61 +
62 + /**
63 + * Contructor
64 + *
65 + * @param Plan|null $plan - Plan object.
66 + * @param \Automattic\Jetpack\Connection\Manager|null $connection_manager - Connection_Manager object.
67 + */
68 + public function __construct( $plan = null, $connection_manager = null ) {
69 + $this->plan = $plan === null ? new Plan() : $plan;
70 + $this->connection_manager = $connection_manager === null ? new Connection_Manager( Package::SLUG ) : $connection_manager;
71 + if ( ! did_action( 'jetpack_search_module_control_initialized' ) ) {
72 + add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'search_filter_available_modules' ), 10, 1 );
73 + if ( Helper::is_wpcom() ) {
74 + add_filter( 'jetpack_active_modules', array( $this, 'search_filter_available_modules' ), 10, 2 );
75 + }
76 + /**
77 + * Fires when the Automattic\Jetpack\Search\Module_Control is initialized for the first time.
78 + */
79 + do_action( 'jetpack_search_module_control_initialized' );
80 + }
81 + }
82 +
83 + /**
84 + * Returns a boolean for whether of the module is enabled.
85 + *
86 + * @return bool
87 + */
88 + public function is_active() {
89 + return ( new Modules() )->is_active( self::JETPACK_SEARCH_MODULE_SLUG );
90 + }
91 +
92 + /**
93 + * Returns a boolean for whether instant search is enabled.
94 + *
95 + * Reads the canonical `jetpack_search_experience` option first — only the
96 + * `'overlay'` experience enables Instant Search, and an explicit `'inline'`
97 + * or `'embedded'` value means the user opted out regardless of the legacy
98 + * boolean. Falls back to the legacy `instant_search_enabled` option only
99 + * when the experience option has never been written (pre-existing sites
100 + * that haven't saved via the new UI).
101 + *
102 + * Module-inactive sites always return false: when Search is off the
103 + * experience option may still hold the user's prior preference (preserved
104 + * for re-enable), and we don't want a stale `'overlay'` to read as
105 + * "Instant Search is on" while the module isn't loaded.
106 + *
107 + * Keeps callers that still read the legacy boolean (debug-bar, AI Chat
108 + * editor state, etc.) correct without each having to migrate to
109 + * `get_experience()` individually.
110 + *
111 + * @return bool
112 + */
113 + public function is_instant_search_enabled() {
114 + if ( ! $this->plan->supports_instant_search() || ! $this->is_active() ) {
115 + return false;
116 + }
117 +
118 + $saved = get_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, false );
119 + if ( self::EXPERIENCE_OVERLAY === $saved ) {
120 + return true;
121 + }
122 + if (
123 + self::EXPERIENCE_INLINE === $saved
124 + || self::EXPERIENCE_EMBEDDED === $saved
125 + || self::EXPERIENCE_OVERLAY_BLOCKS === $saved
126 + ) {
127 + return false;
128 + }
129 +
130 + // Legacy fallback: experience never written, trust the legacy boolean.
131 + return (bool) get_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY );
132 + }
133 +
134 + /**
135 + * Returns a boolean for whether new inline search is enabled.
136 + *
137 + * @return bool
138 + */
139 + public function is_swap_classic_to_inline_search() {
140 + return (bool) get_option( self::SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY, false );
141 + }
142 +
143 + /**
144 + * Activiate Search module
145 + */
146 + public function activate() {
147 + $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
148 + if ( ( new Status() )->is_offline_mode() ) {
149 + return new WP_Error( 'site_offline', __( 'Jetpack Search cannot be used in offline mode.', 'jetpack-search-pkg' ) );
150 + }
151 + if ( ! $is_wpcom && ! $this->connection_manager->is_connected() ) {
152 + return new WP_Error( 'connection_required', __( 'Connect your site to use Jetpack Search.', 'jetpack-search-pkg' ) );
153 + }
154 + if ( ! $this->plan->supports_search() ) {
155 + return new WP_Error( 'not_supported', __( 'Your plan does not support Jetpack Search.', 'jetpack-search-pkg' ) );
156 + }
157 +
158 + $success = ( new Modules() )->activate( self::JETPACK_SEARCH_MODULE_SLUG, false, false );
159 + if ( false === $success ) {
160 + return new WP_Error( 'not_updated', __( 'Setting not updated.', 'jetpack-search-pkg' ) );
161 + }
162 + return $success;
163 + }
164 +
165 + /**
166 + * Deactiviate Search module
167 + */
168 + public function deactivate() {
169 + $success = ( new Modules() )->deactivate( self::JETPACK_SEARCH_MODULE_SLUG );
170 +
171 + $this->disable_instant_search();
172 +
173 + return $success;
174 + }
175 +
176 + /**
177 + * Update module status
178 + *
179 + * @param boolean $active - true to activate, false to deactivate.
180 + */
181 + public function update_status( $active ) {
182 + return $active ? $this->activate() : $this->deactivate();
183 + }
184 +
185 + /**
186 + * Disable Instant Search Experience
187 + */
188 + public function disable_instant_search() {
189 + return update_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY, false );
190 + }
191 +
192 + /**
193 + * Enable Instant Search Experience
194 + *
195 + * Also writes `'overlay'` to the canonical experience option so callers that
196 + * arrive here through the legacy entry points (`update_instant_search_status`,
197 + * the legacy auto-setup REST endpoint, etc.) leave the option in agreement
198 + * with the boolean. Without this, a stale `'embedded'` / `'inline'` in the
199 + * experience option keeps `is_instant_search_enabled()` returning false even
200 + * though the legacy boolean was just set true.
201 + */
202 + public function enable_instant_search() {
203 + if ( ! $this->is_active() ) {
204 + return new WP_Error( 'search_module_inactive', __( 'Search module needs to be activated before enabling instant search.', 'jetpack-search-pkg' ) );
205 + }
206 + if ( ! $this->plan->supports_instant_search() ) {
207 + return new WP_Error( 'not_supported', __( 'Your plan does not support Instant Search.', 'jetpack-search-pkg' ) );
208 + }
209 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_OVERLAY );
210 + return update_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY, true );
211 + }
212 +
213 + /**
214 + * Update instant search status
215 + *
216 + * When disabling via this legacy path we also clear the canonical experience
217 + * option — the user is asking for "not Overlay", and leaving a stale
218 + * `'overlay'` in the option would keep `is_instant_search_enabled()`
219 + * returning true. We don't clear from `disable_instant_search` itself
220 + * because `deactivate()` calls that to flip the legacy boolean, and
221 + * deactivation is meant to preserve the user's experience choice for the
222 + * next re-activation.
223 + *
224 + * @param boolean $enabled - true to enable, false to disable.
225 + */
226 + public function update_instant_search_status( $enabled ) {
227 + if ( ! $enabled ) {
228 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, '' );
229 + }
230 + return $enabled ? $this->enable_instant_search() : $this->disable_instant_search();
231 + }
232 +
233 + /**
234 + * Update setting indicating whether inline search should use newer 1.3 API.
235 + *
236 + * @param bool $swap_classic_to_inline_search - true to use Inline Search, false to use Classic Search.
237 + */
238 + public function update_swap_classic_to_inline_search( bool $swap_classic_to_inline_search ) {
239 + return update_option( self::SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY, $swap_classic_to_inline_search );
240 + }
241 +
242 + /**
243 + * Get the active search experience.
244 + *
245 + * `'off'` is read from the global Jetpack module-active state (not stored in
246 + * this package's option). `'inline'`, `'embedded'`, `'overlay'`, and
247 + * `'overlay_blocks'` are each written as their literal value to
248 + * `jetpack_search_experience` whenever the experience changes, and read
249 + * straight back here.
250 + *
251 + * @return string One of 'embedded', 'overlay', 'overlay_blocks', 'inline', 'off'.
252 + */
253 + public function get_experience() {
254 + if ( ! $this->is_active() ) {
255 + return self::EXPERIENCE_OFF;
256 + }
257 +
258 + $saved = get_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, false );
259 + if ( self::EXPERIENCE_EMBEDDED === $saved ) {
260 + return self::EXPERIENCE_EMBEDDED;
261 + }
262 + if ( self::EXPERIENCE_OVERLAY === $saved ) {
263 + return self::EXPERIENCE_OVERLAY;
264 + }
265 + if ( self::EXPERIENCE_OVERLAY_BLOCKS === $saved ) {
266 + return self::EXPERIENCE_OVERLAY_BLOCKS;
267 + }
268 +
269 + // Legacy fallback for sites that have never saved via the new UI: a true
270 + // `instant_search_enabled` boolean reads as overlay; otherwise inline.
271 + if ( $this->is_instant_search_enabled() ) {
272 + return self::EXPERIENCE_OVERLAY;
273 + }
274 +
275 + return self::EXPERIENCE_INLINE;
276 + }
277 +
278 + /**
279 + * Update the search experience.
280 + *
281 + * Storage is narrower than the wire format: `'off'` only deactivates the global
282 + * module (no write to the experience option) and disables
283 + * `instant_search_enabled` so the legacy boolean doesn't drift true after
284 + * a non-overlay re-enable. `'inline'` deletes the experience option (the
285 + * absence of an opt-in *is* inline). Only `'embedded'` and `'overlay'`
286 + * write affirmative values.
287 + *
288 + * Legacy `module_active` / `instant_search_enabled` are kept in lockstep so
289 + * unmigrated readers (Initializer, Options, sidebar registration) continue to
290 + * see the right state until they're migrated to consult get_experience().
291 + *
292 + * @param string $experience One of 'embedded', 'overlay', 'inline', 'off'.
293 + * @return bool|WP_Error WP_Error on failure; true on success for the affirmative
294 + * branches; the bool from Modules::deactivate() for `'off'`
295 + * (false signals the module was already inactive — a benign
296 + * no-op the REST controller treats as success).
297 + */
298 + public function update_experience( string $experience ) {
299 + $valid_values = array(
300 + self::EXPERIENCE_OVERLAY,
301 + self::EXPERIENCE_OVERLAY_BLOCKS,
302 + self::EXPERIENCE_EMBEDDED,
303 + self::EXPERIENCE_INLINE,
304 + self::EXPERIENCE_OFF,
305 + );
306 + if ( ! in_array( $experience, $valid_values, true ) ) {
307 + return new WP_Error(
308 + 'invalid_experience',
309 + esc_html__( 'Invalid experience value.', 'jetpack-search-pkg' ),
310 + array( 'status' => 400 )
311 + );
312 + }
313 +
314 + // Defence-in-depth for the experimental blocks-powered overlay: the
315 + // dashboard already hides the option when the
316 + // `jetpack_search_overlay_block_template_enabled` filter is off, but
317 + // a scripted REST POST could otherwise pre-stage `overlay_blocks` on
318 + // a site where the operator has pinned the filter to false. Reject
319 + // the value at the boundary so the runtime gate isn't the only
320 + // safety net.
321 + if (
322 + self::EXPERIENCE_OVERLAY_BLOCKS === $experience
323 + && ! (bool) apply_filters( 'jetpack_search_overlay_block_template_enabled', true )
324 + ) {
325 + return new WP_Error(
326 + 'experience_not_available',
327 + esc_html__( 'The blocks-powered Overlay search experience is not enabled on this site.', 'jetpack-search-pkg' ),
328 + array( 'status' => 400 )
329 + );
330 + }
331 +
332 + switch ( $experience ) {
333 + case self::EXPERIENCE_OFF:
334 + $this->disable_instant_search();
335 + return ( new Modules() )->deactivate( self::JETPACK_SEARCH_MODULE_SLUG );
336 +
337 + case self::EXPERIENCE_INLINE:
338 + $result = $this->activate();
339 + if ( is_wp_error( $result ) ) {
340 + return $result;
341 + }
342 + $this->disable_instant_search();
343 + // Inline is the absence of an opt-in — delete the option rather than
344 + // writing 'inline'. Pre-existing sites that have never saved are
345 + // already in this state, so this also normalises after a switch.
346 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, '' );
347 + return true;
348 +
349 + case self::EXPERIENCE_EMBEDDED:
350 + $result = $this->activate();
351 + if ( is_wp_error( $result ) ) {
352 + return $result;
353 + }
354 + $this->disable_instant_search();
355 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_EMBEDDED );
356 + return true;
357 +
358 + case self::EXPERIENCE_OVERLAY:
359 + $result = $this->activate();
360 + if ( is_wp_error( $result ) ) {
361 + return $result;
362 + }
363 + $result = $this->enable_instant_search();
364 + if ( is_wp_error( $result ) ) {
365 + return $result;
366 + }
367 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_OVERLAY );
368 + return true;
369 +
370 + case self::EXPERIENCE_OVERLAY_BLOCKS:
371 + // The blocks-powered overlay does not boot the legacy preact
372 + // `SearchApp`, so we keep `instant_search_enabled` off; the
373 + // runtime hookup lives in `Search_Blocks::is_block_template_overlay_enabled()`
374 + // which combines the user's experience choice with the
375 + // `jetpack_search_overlay_block_template_enabled` server filter.
376 + $result = $this->activate();
377 + if ( is_wp_error( $result ) ) {
378 + return $result;
379 + }
380 + $this->disable_instant_search();
381 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_OVERLAY_BLOCKS );
382 + return true;
383 +
384 + default:
385 + // Unreachable — the `in_array` guard at the top of this
386 + // method already restricts $experience to the cases above.
387 + // Returning a typed value (rather than falling through to an
388 + // implicit `null`) satisfies the static analyzers that flag
389 + // switch statements without a `default` branch, and keeps the
390 + // method's `bool|WP_Error` contract honest if a future caller
391 + // loosens the validation.
392 + return true;
393 + }
394 + }
395 +
396 + /**
397 + * Get a list of activated modules as an array of module slugs.
398 + *
399 + * @deprecated 0.12.3
400 + * @return Array $active_modules
401 + */
402 + public function get_active_modules() {
403 + _deprecated_function(
404 + __METHOD__,
405 + 'jetpack-search-0.12.3',
406 + 'Automattic\\Jetpack\\Modules\\get_active'
407 + );
408 +
409 + return ( new Modules() )->get_active();
410 + }
411 +
412 + /**
413 + * Adds search to the list of available modules
414 + *
415 + * @param array $modules The available modules.
416 + * @return array
417 + */
418 + public function search_filter_available_modules( $modules ) {
419 + return array_merge( array( self::JETPACK_SEARCH_MODULE_SLUG ), $modules );
420 + }
421 +}