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