PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 / dashboard / class-initial-state.php

class-initial-state.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-search/src/dashboard/class-initial-state.php

479 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The React initial state.
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\Status;
12 use Jetpack_Options;
13
14 /**
15 * The React initial state.
16 */
17 class Initial_State {
18
19 /**
20 * Connection Manager
21 *
22 * @var Connection_Manager
23 */
24 protected $connection_manager;
25
26 /**
27 * Search Module Control
28 *
29 * @var Module_Control
30 */
31 protected $module_control;
32
33 /**
34 * Constructor
35 *
36 * @param Connection_Manager $connection_manager - Connection mananger instance.
37 * @param Module_Control $module_control - Module control instance.
38 */
39 public function __construct( $connection_manager = null, $module_control = null ) {
40 $this->connection_manager = $connection_manager ? $connection_manager : new Connection_Manager( Package::SLUG );
41 $this->module_control = $module_control ? $module_control : new Module_Control();
42 }
43
44 /**
45 * Render JS for the initial state
46 *
47 * @return string - JS string.
48 */
49 public function render() {
50 return 'var JETPACK_SEARCH_DASHBOARD_INITIAL_STATE=' . wp_json_encode( $this->get_initial_state(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
51 }
52
53 /**
54 * Get the initial state data.
55 *
56 * @return array
57 */
58 public function get_initial_state() {
59 return array(
60 'siteData' => array(
61 'WP_API_root' => esc_url_raw( rest_url() ),
62 'wpcomOriginApiUrl' => $this->get_wp_api_root(),
63 'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
64 'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
65 'purchaseToken' => $this->get_purchase_token(),
66 /**
67 * Whether promotions are visible or not.
68 *
69 * @param bool $are_promotions_active Status of promotions visibility. True by default.
70 */
71 'showPromotions' => apply_filters( 'jetpack_show_promotions', true ),
72 'adminUrl' => esc_url( admin_url() ),
73 'readerChatGuidelinesUrl' => $this->get_reader_chat_guidelines_url(),
74 'aiAgentAccessAvailable' => $this->is_ai_agent_access_available(),
75 'aiAgentAccessGuidelinesUrl' => $this->get_ai_agent_access_guidelines_url(),
76 'blogId' => Jetpack_Options::get_option( 'id', 0 ),
77 'version' => Package::VERSION,
78 'calypsoSlug' => ( new Status() )->get_site_suffix(),
79 'title' => get_bloginfo( 'name' ),
80 'postTypes' => $this->get_post_types_with_labels(),
81 'isWpcom' => Helper::is_wpcom(),
82 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
83 'isPlanJustUpgraded' => isset( $_GET['just_upgraded'] ) && wp_unslash( $_GET['just_upgraded'] ),
84 /**
85 * Whether the Jetpack Search 3.0 Interactivity API blocks are enabled.
86 * Mirrors the `jetpack_search_blocks_enabled` server-side filter so the
87 * dashboard React app can gate the new feature-selection UI on the
88 * same flag the back end uses to register the blocks themselves.
89 */
90 'searchBlocksEnabled' => (bool) apply_filters( 'jetpack_search_blocks_enabled', true ),
91 /**
92 * Whether the experimental blocks-powered Overlay search experience
93 * is available in the Experience Selector. Mirrors the
94 * `jetpack_search_overlay_block_template_enabled` server-side filter
95 * so the dashboard React app can gate the new card on the same flag
96 * the back end uses to enable the runtime swap. Defaults to true so
97 * the Beta card ships to every site; operators that pin the filter
98 * to false fall back to the original four-card selector.
99 */
100 'blockOverlayEnabled' => (bool) apply_filters( 'jetpack_search_overlay_block_template_enabled', true ),
101 /**
102 * Editor affordances for the experimental blocks-powered overlay.
103 * Surfaces in the new Overlay search card so admins can edit the
104 * rendered template via the standard block editor on `post.php` —
105 * works on both block and classic themes. URLs are null for any
106 * visitor without `manage_options`; the action handlers also
107 * enforce that capability server-side.
108 */
109 'blockTemplateOverlay' => $this->get_block_template_overlay_config(),
110 /**
111 * Same as `blockTemplateOverlay` but for the WooCommerce product
112 * variant of the overlay — the Overlay card surfaces a second
113 * "Edit the product Search overlay" entry on Woo stores so the
114 * product-search overlay template is customizable too.
115 */
116 'productOverlayTemplate' => $this->get_product_overlay_template_config(),
117 /**
118 * Editor affordances for the classic-theme search-template
119 * singleton CPT. The Embedded card surfaces these on
120 * classic themes (which can't reach the Site Editor) so
121 * admins still get an "Edit search template" entry — same
122 * shape and same React link as `blockTemplateOverlay`.
123 */
124 'searchTemplate' => $this->get_search_template_config(),
125 /**
126 * Same as `searchTemplate` but for the WooCommerce product
127 * search shim — the `WooCommerceProductSearchControl`
128 * surfaces this on classic themes so the "Edit the product
129 * search template" link routes to `post.php` on the hidden
130 * CPT instead of a useless Site Editor URL.
131 */
132 'productSearchTemplate' => $this->get_product_search_template_config(),
133 // Gates the WooCommerce Product Search control to stores.
134 'isWooCommerceActive' => Search_Blocks::woocommerce_blocks_enabled(),
135 /**
136 * Active theme stylesheet — used by the experience-selector to deep-link
137 * the "Edit search template" action to the right Site Editor entry
138 * (`?p=/wp_template/<stylesheet>//jetpack-search`).
139 */
140 'activeThemeStylesheet' => get_stylesheet(),
141 /**
142 * Whether the active theme is a block theme. Embedded itself
143 * works on every theme — block themes through the FSE
144 * `search_template_hierarchy` route, classic themes through
145 * the singleton-CPT shim — but the Embedded card's
146 * customization affordances diverge: block themes get the
147 * Site-Editor entry points ("Edit search template" / "Insert
148 * pattern"), classic themes get the block-editor-on-a-hidden-
149 * CPT path via `searchTemplate`. This flag is the dashboard's
150 * branch selector for which to render.
151 */
152 'themeSupportsBlocks' => wp_is_block_theme(),
153 ),
154 'userData' => array(
155 'currentUser' => $this->current_user_data(),
156 ),
157 'jetpackSettings' => array(
158 'search' => $this->module_control->is_active(),
159 'instant_search_enabled' => $this->module_control->is_instant_search_enabled(),
160 'experience' => $this->module_control->get_experience(),
161 ),
162 'features' => array_map(
163 'sanitize_text_field',
164 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
165 isset( $_GET['features'] ) ? explode( ',', wp_unslash( $_GET['features'] ) ) : array()
166 ),
167 );
168 }
169
170 /**
171 * Get API root.
172 *
173 * It return first party API root for WPCOM simple sites.
174 */
175 protected function get_wp_api_root() {
176 if ( ! Helper::is_wpcom() ) {
177 return esc_url_raw( rest_url() );
178 }
179 // First party API prefix for WPCOM.
180 return esc_url_raw( site_url( '/wp-json/wpcom-origin/' ) );
181 }
182
183 /**
184 * Get the Reader Chat guidelines admin page URL when it is registered.
185 *
186 * The guidelines page is controlled outside Jetpack. Returning an empty
187 * URL lets the dashboard hide the link when that experiment is unavailable.
188 *
189 * @return string Guidelines admin URL, or an empty string when unavailable.
190 */
191 protected function get_reader_chat_guidelines_url() {
192 return $this->get_guidelines_url();
193 }
194
195 /**
196 * Get the AI Agent Access guidelines admin page URL when it is registered.
197 *
198 * The guidelines page is controlled outside Jetpack. Returning an empty
199 * URL lets the dashboard hide the link when that page is unavailable.
200 *
201 * @return string Guidelines admin URL, or an empty string when unavailable.
202 */
203 protected function get_ai_agent_access_guidelines_url() {
204 return $this->get_guidelines_url();
205 }
206
207 /**
208 * Get the Guidelines admin page URL when it is registered.
209 *
210 * @return string Guidelines admin URL, or an empty string when unavailable.
211 */
212 protected function get_guidelines_url() {
213 if ( ! function_exists( 'menu_page_url' ) ) {
214 return '';
215 }
216
217 return esc_url_raw( menu_page_url( 'guidelines-wp-admin', false ) );
218 }
219
220 /**
221 * Check whether the AI Agent Access toggle should be available.
222 *
223 * Private sites are not eligible because external AI agents cannot read
224 * their public content.
225 *
226 * @return bool
227 */
228 protected function is_ai_agent_access_available() {
229 return ! $this->is_private_site();
230 }
231
232 /**
233 * Build the block-template overlay editor config exposed to the dashboard.
234 *
235 * `enabled` mirrors `Search_Blocks::is_block_template_overlay_enabled()`
236 * — true only when the user is currently *on* the blocks Overlay arm.
237 * The editor URLs, by contrast, are gated on the operator filter alone
238 * so admins can edit the template from the Beta card even before
239 * activating it (otherwise the React store, computed at page load
240 * before any switch, would carry a null `editorUrl` and the link would
241 * become a no-op once the card flipped to Active without a refresh).
242 *
243 * @return array{enabled: bool, editorUrl: string|null, postType: string|null, isCustomized: bool}
244 */
245 protected function get_block_template_overlay_config(): array {
246 return $this->build_singleton_template_config(
247 Search_Blocks::is_block_template_overlay_enabled(),
248 Search_Blocks::is_block_template_overlay_filter_on() && current_user_can( 'manage_options' ),
249 Overlay_Template::class
250 );
251 }
252
253 /**
254 * Build the product-overlay editor config exposed to the dashboard.
255 * Counterpart of `get_block_template_overlay_config()` for the WooCommerce
256 * product variant. `enabled` adds the override-on + WC check on top of the
257 * overlay-arm gate — it signals the product overlay's front-end render path
258 * is actually wired. `$can_edit` mirrors the init gate: the editable CPT
259 * only registers on Woo stores with the overlay filter on (see
260 * `Search_Blocks::init()`), so exposing the editor URL off Woo would produce
261 * a link that silently does nothing.
262 *
263 * @return array{enabled: bool, editorUrl: string|null, postType: string|null, isCustomized: bool}
264 */
265 protected function get_product_overlay_template_config(): array {
266 $wc_enabled = Search_Blocks::woocommerce_blocks_enabled();
267 return $this->build_singleton_template_config(
268 Search_Blocks::is_block_template_overlay_enabled()
269 && $wc_enabled
270 && Search_Blocks::woocommerce_search_template_override_enabled(),
271 Search_Blocks::is_block_template_overlay_filter_on()
272 && $wc_enabled
273 && current_user_can( 'manage_options' ),
274 Product_Overlay_Template::class
275 );
276 }
277
278 /**
279 * Build the classic-theme search-template editor config exposed to the
280 * dashboard. Counterpart of `get_block_template_overlay_config()` —
281 * same `{enabled, editorUrl, postType, isCustomized}` shape and the
282 * same "expose URLs before activation" rule: admins can edit the
283 * template from the Embedded card on any classic theme, even before
284 * actually switching to Embedded.
285 *
286 * @return array{enabled: bool, editorUrl: string|null, postType: string|null, isCustomized: bool}
287 */
288 protected function get_search_template_config(): array {
289 $is_classic = ! wp_is_block_theme();
290 return $this->build_singleton_template_config(
291 $is_classic && Module_Control::EXPERIENCE_EMBEDDED === $this->module_control->get_experience(),
292 $is_classic && current_user_can( 'manage_options' ),
293 Search_Template::class
294 );
295 }
296
297 /**
298 * Build the classic-theme product-search-template editor config exposed
299 * to the dashboard. Counterpart of `get_search_template_config()` for the
300 * WooCommerce product shim — same `{enabled, editorUrl, postType, isCustomized}`
301 * shape. `enabled` adds the override-on check on top of the classic +
302 * server-rendered-experience gate — it signals "the front-end render path
303 * is actually wired", distinct from "the override toggle is on" in
304 * `jetpackSettings.override_woocommerce_search_template`. `$can_edit`
305 * mirrors the init gate: `Product_Search_Template::init()` (in
306 * `Search_Blocks::init()`) registers `maybe_handle_editor_request` on
307 * classic Embedded **and** classic Inline (both route the product shim),
308 * so the editor URL surfaces for both — but not on a block theme, where
309 * the Site Editor link is used instead.
310 *
311 * @return array{enabled: bool, editorUrl: string|null, postType: string|null, isCustomized: bool}
312 */
313 protected function get_product_search_template_config(): array {
314 $experience = $this->module_control->get_experience();
315 $is_classic = ! wp_is_block_theme();
316 // Embedded and Inline both route the classic product shim, so both
317 // expose the CPT editor.
318 $is_classic_product_override = $is_classic
319 && in_array(
320 $experience,
321 array( Module_Control::EXPERIENCE_EMBEDDED, Module_Control::EXPERIENCE_INLINE ),
322 true
323 );
324 return $this->build_singleton_template_config(
325 $is_classic_product_override && Search_Blocks::woocommerce_search_template_override_enabled(),
326 $is_classic_product_override && current_user_can( 'manage_options' ),
327 Product_Search_Template::class
328 );
329 }
330
331 /**
332 * Shared assembly for a {@see Singleton_Template_Cpt}-backed editor
333 * config block. Both `blockTemplateOverlay` and `searchTemplate` ship
334 * the same `{enabled, editorUrl, postType, isCustomized}` shape to
335 * the React dashboard. The `$enabled` and `$can_edit` flags are
336 * intentionally separate: the card lights up as "active" on
337 * `$enabled`, but the URLs / `postType` surface as soon as `$can_edit`
338 * is true so admins can pre-customize the template before activating
339 * the experience — without forcing a page reload after the switch.
340 *
341 * `postType` (rather than a pre-built REST path) is what the dashboard
342 * needs: the React "Restore default" handler builds the
343 * `${wpcomOriginApiUrl}jetpack/v4/search/templates/<post_type>` URL
344 * itself, which is how the request reaches the right route on wpcom
345 * Simple sites (the local /wp-json/ surface there doesn't expose
346 * Jetpack routes). The handler re-checks `manage_options` server-side,
347 * so omitting the URLs / postType here just keeps the wire payload
348 * tight for non-admins.
349 *
350 * @param bool $enabled Whether the CPT-backed route is currently live on the site.
351 * @param bool $can_edit Whether to expose the editor URL + postType (operator-level gate + capability).
352 * @param string $cpt_class Concrete `Singleton_Template_Cpt` subclass to query.
353 * @return array{enabled: bool, editorUrl: string|null, postType: string|null, isCustomized: bool}
354 */
355 protected function build_singleton_template_config( bool $enabled, bool $can_edit, string $cpt_class ): array {
356 return array(
357 'enabled' => $enabled,
358 'editorUrl' => $can_edit ? $cpt_class::get_editor_url() : null,
359 'postType' => $can_edit ? $cpt_class::POST_TYPE : null,
360 'isCustomized' => $can_edit && $cpt_class::is_customized(),
361 );
362 }
363
364 /**
365 * Check whether the current site is private.
366 *
367 * @return bool
368 */
369 protected function is_private_site() {
370 if ( function_exists( 'is_private_blog' ) ) {
371 return (bool) \is_private_blog();
372 }
373
374 return -1 === (int) get_option( 'blog_public', 1 );
375 }
376
377 /**
378 * Gather data about the current user.
379 *
380 * @return array
381 */
382 protected function current_user_data() {
383 $current_user = wp_get_current_user();
384 $is_user_connected = $this->connection_manager->is_user_connected( $current_user->ID );
385 $is_master_user = $is_user_connected && (int) $current_user->ID && (int) Jetpack_Options::get_option( 'master_user' ) === (int) $current_user->ID;
386 $dotcom_data = $this->connection_manager->get_connected_user_data();
387
388 $current_user_data = array(
389 'isConnected' => $is_user_connected,
390 'isMaster' => $is_master_user,
391 'username' => $current_user->user_login,
392 'id' => $current_user->ID,
393 'wpcomUser' => $dotcom_data,
394 'permissions' => array(
395 'manage_options' => current_user_can( 'manage_options' ),
396 ),
397 );
398
399 return $current_user_data;
400 }
401
402 /**
403 * Gets the post type labels for all of the site's post types (including custom post types)
404 *
405 * @return array
406 */
407 protected function get_post_types_with_labels() {
408
409 $args = array(
410 'public' => true,
411 );
412
413 $post_types_with_labels = array();
414
415 $post_types = get_post_types( $args, 'objects' );
416
417 // We don't need all the additional post_type data, just the slug & label
418 foreach ( $post_types as $post_type ) {
419 $post_type_with_label = array(
420 'slug' => $post_type->name,
421 'label' => $post_type->label,
422 );
423
424 $post_types_with_labels[ $post_type->name ] = $post_type_with_label;
425 }
426 return $post_types_with_labels;
427 }
428
429 /**
430 * Gets a purchase token that is used for Jetpack logged out visitor checkout.
431 * The purchase token should be appended to all CTA url's that lead to checkout.
432 *
433 * @return string|boolean
434 */
435 protected function get_purchase_token() {
436 if ( ! $this->current_user_can_purchase() ) {
437 return false;
438 }
439
440 $purchase_token = Jetpack_Options::get_option( 'purchase_token', false );
441
442 if ( $purchase_token ) {
443 return $purchase_token;
444 }
445 // If the purchase token is not saved in the options table yet, then add it.
446 Jetpack_Options::update_option( 'purchase_token', $this->generate_purchase_token(), true );
447 return Jetpack_Options::get_option( 'purchase_token', false );
448 }
449
450 /**
451 * Generates a purchase token that is used for Jetpack logged out visitor checkout.
452 *
453 * @return string
454 */
455 protected function generate_purchase_token() {
456 return wp_generate_password( 12, false );
457 }
458
459 /**
460 * Determine if the current user is allowed to make Jetpack purchases without
461 * a WordPress.com account
462 *
463 * @return boolean True if the user can make purchases, false if not
464 */
465 public function current_user_can_purchase() {
466 // The site must be site-connected to Jetpack (no users connected).
467 if ( ! $this->connection_manager->is_site_connection() ) {
468 return false;
469 }
470
471 // Make sure only administrators can make purchases.
472 if ( ! current_user_can( 'manage_options' ) ) {
473 return false;
474 }
475
476 return true;
477 }
478 }
479