Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.17% covered (warning)
65.17%
58 / 89
52.63% covered (warning)
52.63%
10 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Search
65.52% covered (warning)
65.52%
57 / 87
52.63% covered (warning)
52.63%
10 / 19
93.13
0.00% covered (danger)
0.00%
0 / 1
 get_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_title
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_long_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_features
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_pricing_for_ui
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
2.01
 get_post_checkout_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_wpcom_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wpcom_free_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_new_pricing_202208
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 get_status
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_pricing_from_wpcom
59.26% covered (warning)
59.26%
16 / 27
0.00% covered (danger)
0.00%
0 / 1
12.33
 has_trial_support
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_paid_plan_product_slugs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 has_free_plan_for_product
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
14.79
 do_product_specific_activation
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_post_activation_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_manage_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_upgradable_by_bundle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Search product
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack\Products;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Automattic\Jetpack\Constants;
13use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
14use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
15use Automattic\Jetpack\Search\Module_Control as Search_Module_Control;
16use WP_Error;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22/**
23 * Class responsible for handling the Search product
24 */
25class Search extends Hybrid_Product {
26    /**
27     * The product slug
28     *
29     * @var string
30     */
31    public static $slug = 'search';
32
33    /**
34     * The Jetpack module name
35     *
36     * @var string
37     */
38    public static $module_name = 'search';
39
40    /**
41     * The slug of the plugin associated with this product.
42     *
43     * @var string
44     */
45    public static $plugin_slug = 'jetpack-search';
46
47    /**
48     * The category of the product
49     *
50     * @var string
51     */
52    public static $category = 'performance';
53
54    /**
55     * Search has a standalone plugin
56     *
57     * @var bool
58     */
59    public static $has_standalone_plugin = true;
60
61    /**
62     * Whether this product has a free offering
63     *
64     * @var bool
65     */
66    public static $has_free_offering = true;
67
68    /**
69     * Whether this product requires a plan to work at all
70     *
71     * @var bool
72     */
73    public static $requires_plan = true;
74
75    /**
76     * The filename (id) of the plugin associated with this product.
77     *
78     * @var string
79     */
80    public static $plugin_filename = array(
81        'jetpack-search/jetpack-search.php',
82        'search/jetpack-search.php',
83        'jetpack-search-dev/jetpack-search.php',
84    );
85
86    /**
87     * Search only requires site connection
88     *
89     * @var boolean
90     */
91    public static $requires_user_connection = true;
92
93    /**
94     * The feature slug that identifies the paid plan
95     *
96     * @var string
97     */
98    public static $feature_identifying_paid_plan = 'search';
99
100    /**
101     * Get the product name
102     *
103     * @return string
104     */
105    public static function get_name() {
106        return 'Search';
107    }
108
109    /**
110     * Get the product title
111     *
112     * @return string
113     */
114    public static function get_title() {
115        return 'Jetpack Search';
116    }
117
118    /**
119     * Get the internationalized product description
120     *
121     * @return string
122     */
123    public static function get_description() {
124        return __( 'Instantly deliver the most relevant results to your visitors.', 'jetpack-my-jetpack' );
125    }
126
127    /**
128     * Get the internationalized product long description
129     *
130     * @return string
131     */
132    public static function get_long_description() {
133        return __( 'Help your site visitors find answers instantly so they keep reading and buying. Great for sites with a lot of content.', 'jetpack-my-jetpack' );
134    }
135
136    /**
137     * Get the internationalized features list
138     *
139     * @return array Boost features list
140     */
141    public static function get_features() {
142        return array(
143            __( 'Instant search and indexing', 'jetpack-my-jetpack' ),
144            __( 'Powerful filtering', 'jetpack-my-jetpack' ),
145            __( 'Supports 38 languages', 'jetpack-my-jetpack' ),
146            __( 'Spelling correction', 'jetpack-my-jetpack' ),
147        );
148    }
149
150    /**
151     * Get the product princing details
152     *
153     * @return array Pricing details
154     */
155    public static function get_pricing_for_ui() {
156        // Basic pricing info.
157        $pricing = array_merge(
158            array(
159                'available'               => true,
160                'trial_available'         => static::has_trial_support(),
161                'wpcom_product_slug'      => static::get_wpcom_product_slug(),
162                'wpcom_free_product_slug' => static::get_wpcom_free_product_slug(),
163            ),
164            Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
165        );
166
167        $record_count   = intval( Search_Stats::estimate_count() );
168        $search_pricing = static::get_pricing_from_wpcom( $record_count );
169
170        if ( is_wp_error( $search_pricing ) ) {
171            return $pricing;
172        }
173
174        $pricing['estimated_record_count'] = $record_count;
175
176        return array_merge( $pricing, $search_pricing );
177    }
178
179    /**
180     * Get the URL the user is taken after purchasing the product through the checkout
181     *
182     * @return ?string
183     */
184    public static function get_post_checkout_url() {
185        return self::get_manage_url();
186    }
187
188    /**
189     * Get the WPCOM product slug used to make the purchase
190     *
191     * @return ?string
192     */
193    public static function get_wpcom_product_slug() {
194        return 'jetpack_search';
195    }
196
197    /**
198     * Get the WPCOM free product slug
199     *
200     * @return ?string
201     */
202    public static function get_wpcom_free_product_slug() {
203        return 'jetpack_search_free';
204    }
205
206    /**
207     * Returns true if the new_pricing_202208 is set to not empty in URL for testing purpose, or it's active.
208     */
209    public static function is_new_pricing_202208() {
210        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
211        if ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) {
212            return true;
213        }
214
215        $record_count   = intval( Search_Stats::estimate_count() );
216        $search_pricing = static::get_pricing_from_wpcom( $record_count );
217        if ( is_wp_error( $search_pricing ) ) {
218            return false;
219        }
220
221        return '202208' === $search_pricing['pricing_version'];
222    }
223
224    /**
225     * Override status to `needs_activation` when status is `needs_plan`.
226     */
227    public static function get_status() {
228        $status = parent::get_status();
229        return $status;
230    }
231
232    /**
233     * Use centralized Search pricing API.
234     *
235     * The function is also used by the search package, as a result it could be called before site connection - i.e. blog token might not be available.
236     *
237     * @param int $record_count Record count to estimate pricing.
238     *
239     * @return array|WP_Error
240     */
241    public static function get_pricing_from_wpcom( $record_count ) {
242        static $pricings = array();
243        $connection      = new Connection_Manager();
244        $blog_id         = \Jetpack_Options::get_option( 'id' );
245
246        if ( isset( $pricings[ $record_count ] ) ) {
247            return $pricings[ $record_count ];
248        }
249
250        // If the site is connected, request pricing with the blog token
251        if ( $blog_id ) {
252            $endpoint = sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() );
253
254            // If available in the user data, set the user's currency as one of the params
255            if ( $connection->is_user_connected() ) {
256                $user_details = $connection->get_connected_user_data();
257                if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
258                    $endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
259                }
260            }
261
262            $response = Client::wpcom_json_api_request_as_blog(
263                $endpoint,
264                '2',
265                array( 'timeout' => 5 ),
266                null,
267                'wpcom'
268            );
269        } else {
270            $response = wp_remote_get(
271                sprintf( Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ) . '/wpcom/v2/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
272                array( 'timeout' => 5 )
273            );
274        }
275
276        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
277            return new WP_Error( 'search_pricing_fetch_failed' );
278        }
279
280        $body                      = wp_remote_retrieve_body( $response );
281        $pricings[ $record_count ] = json_decode( $body, true );
282        return $pricings[ $record_count ];
283    }
284
285    /**
286     * Checks whether the product supports trial or not
287     *
288     * Returns true if it supports. Return false otherwise.
289     *
290     * Free products will always return false.
291     *
292     * @return boolean
293     */
294    public static function has_trial_support() {
295        return static::is_new_pricing_202208();
296    }
297
298    /**
299     * Get the product-slugs of the paid plans for this product (not including bundles)
300     *
301     * @return array
302     */
303    public static function get_paid_plan_product_slugs() {
304        return array(
305            'jetpack_search',
306            'jetpack_search_monthly',
307            'jetpack_search_bi_yearly',
308        );
309    }
310
311    /**
312     * Checks if the site purchases contain a free search plan
313     *
314     * @return bool
315     */
316    public static function has_free_plan_for_product() {
317        $purchases_data = Wpcom_Products::get_site_current_purchases();
318        if ( is_wp_error( $purchases_data ) ) {
319            return false;
320        }
321        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
322            foreach ( $purchases_data as $purchase ) {
323                if ( str_contains( $purchase->product_slug, 'jetpack_search_free' ) ) {
324                    return true;
325                }
326            }
327        }
328        return false;
329    }
330
331    /**
332     * Activates the product. Try to enable instant search after the Search module was enabled.
333     *
334     * @param bool|WP_Error $product_activation Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
335     * @return bool|WP_Error
336     */
337    public static function do_product_specific_activation( $product_activation ) {
338        $product_activation = parent::do_product_specific_activation( $product_activation );
339        if ( is_wp_error( $product_activation ) ) {
340            return $product_activation;
341        }
342
343        if ( class_exists( 'Automattic\Jetpack\Search\Module_Control' ) ) {
344            ( new Search_Module_Control() )->enable_instant_search();
345        }
346
347        // we don't want to change the success of the activation if we fail to activate instant search. That's not mandatory.
348        return $product_activation;
349    }
350
351    /**
352     * Get the URL the user is taken after activating the product
353     *
354     * @return ?string
355     */
356    public static function get_post_activation_url() {
357        return ''; // stay in My Jetpack page or continue the purchase flow if needed.
358    }
359
360    /**
361     * Get the URL where the user manages the product
362     *
363     * @return ?string
364     */
365    public static function get_manage_url() {
366        return admin_url( 'admin.php?page=jetpack-search' );
367    }
368
369    /**
370     * Return product bundles list
371     * that supports the product.
372     *
373     * @return boolean|array Products bundle list.
374     */
375    public static function is_upgradable_by_bundle() {
376        return array( 'complete' );
377    }
378}