PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-search/src/class-module-control.php +253 -6 13.6.2 → 16.3-beta View file →
@@ -11,10 +11,14 @@
11 11 use Automattic\Jetpack\Modules;
12 12 use Automattic\Jetpack\Status;
13 13 use WP_Error;
14 14
15 +if ( ! defined( 'ABSPATH' ) ) {
16 + exit( 0 );
17 +}
18 +
15 19 /**
16 - * To get and set Searh module settings
20 + * To get and set Search module settings
17 21 */
18 22 class Module_Control {
19 23 /**
20 24 * Plan object
@@ -32,13 +36,31 @@
32 36
33 37 /**
34 38 * We use the same options as Jetpack the plugin to flag whether Search is active.
35 39 */
36 - const JETPACK_ACTIVE_MODULES_OPTION_KEY = 'active_modules';
37 - const JETPACK_SEARCH_MODULE_SLUG = 'search';
38 - const SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY = 'instant_search_enabled';
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';
39 45
40 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 + /**
41 63 * Contructor
42 64 *
43 65 * @param Plan|null $plan - Plan object.
44 66 * @param \Automattic\Jetpack\Connection\Manager|null $connection_manager - Connection_Manager object.
@@ -69,22 +91,65 @@
69 91
70 92 /**
71 93 * Returns a boolean for whether instant search is enabled.
72 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 + *
73 111 * @return bool
74 112 */
75 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.
76 131 return (bool) get_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY );
77 132 }
78 133
79 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 + /**
80 144 * Activiate Search module
81 145 */
82 146 public function activate() {
147 + $is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
83 148 if ( ( new Status() )->is_offline_mode() ) {
84 - return new WP_Error( 'site_offline', __( 'Jetpack Search can not be used in offline mode.', 'jetpack-search-pkg' ) );
149 + return new WP_Error( 'site_offline', __( 'Jetpack Search cannot be used in offline mode.', 'jetpack-search-pkg' ) );
85 150 }
86 - if ( ! $this->connection_manager->is_connected() ) {
151 + if ( ! $is_wpcom && ! $this->connection_manager->is_connected() ) {
87 152 return new WP_Error( 'connection_required', __( 'Connect your site to use Jetpack Search.', 'jetpack-search-pkg' ) );
88 153 }
89 154 if ( ! $this->plan->supports_search() ) {
90 155 return new WP_Error( 'not_supported', __( 'Your plan does not support Jetpack Search.', 'jetpack-search-pkg' ) );
@@ -125,8 +190,15 @@
125 190 }
126 191
127 192 /**
128 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.
129 201 */
130 202 public function enable_instant_search() {
131 203 if ( ! $this->is_active() ) {
132 204 return new WP_Error( 'search_module_inactive', __( 'Search module needs to be activated before enabling instant search.', 'jetpack-search-pkg' ) );
@@ -133,8 +205,9 @@
133 205 }
134 206 if ( ! $this->plan->supports_instant_search() ) {
135 207 return new WP_Error( 'not_supported', __( 'Your plan does not support Instant Search.', 'jetpack-search-pkg' ) );
136 208 }
209 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, self::EXPERIENCE_OVERLAY );
137 210 return update_option( self::SEARCH_MODULE_INSTANT_SEARCH_OPTION_KEY, true );
138 211 }
139 212
140 213 /**
@@ -139,12 +212,186 @@
139 212
140 213 /**
141 214 * Update instant search status
142 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 + *
143 224 * @param boolean $enabled - true to enable, false to disable.
144 225 */
145 226 public function update_instant_search_status( $enabled ) {
227 + if ( ! $enabled ) {
228 + update_option( self::SEARCH_MODULE_EXPERIENCE_OPTION_KEY, '' );
229 + }
146 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 + }
147 394 }
148 395
149 396 /**
150 397 * Get a list of activated modules as an array of module slugs.