PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-search / src / class-module-control.php

class-module-control.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-search/src/class-module-control.php

422 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
422