Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.13% covered (warning)
81.13%
43 / 53
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Plan
82.35% covered (warning)
82.35%
42 / 51
72.73% covered (warning)
72.73%
8 / 11
37.63
0.00% covered (danger)
0.00%
0 / 1
 init_hooks
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_plan_info_from_wpcom
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 get_plan_info
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 has_jetpack_search_product
n/a
0 / 0
n/a
0 / 0
1
 supports_instant_search
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 supports_search
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 must_upgrade
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 supports_only_classic_search
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 ever_supported_search
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_free_plan
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 update_search_plan_info
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 set_plan_options
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
8.30
1<?php
2/**
3 * The Search Plan class.
4 *
5 * @package automattic/jetpack-search
6 */
7
8namespace Automattic\Jetpack\Search;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Status;
12use Jetpack_Options;
13use WP_Error;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19/**
20 * Manage Search plan information.
21 */
22class Plan {
23    const JETPACK_SEARCH_PLAN_INFO_OPTION_KEY  = 'jetpack_search_plan_info';
24    const JETPACK_SEARCH_EVER_SUPPORTED_SEARCH = 'jetpack_search_ever_supported_search';
25
26    // The pricing update starting from August 2022.
27    const JETPACK_SEARCH_NEW_PRICING_VERSION = '202208';
28    const JETPACK_SEARCH_FREE_PRODUCT_SLUG   = 'jetpack_search_free';
29
30    /**
31     * Whether we have hooked the actions.
32     *
33     * @var boolean
34     */
35    protected static $update_plan_hook_initialized = false;
36
37    /**
38     * Init hooks for updating plan info
39     */
40    public function init_hooks() {
41        // Update plan info from WPCOM on Jetpack heartbeat.
42        // TODO: implement heartbeart for search.
43        if ( ! static::$update_plan_hook_initialized ) {
44            add_action( 'jetpack_heartbeat', array( $this, 'get_plan_info_from_wpcom' ) );
45            static::$update_plan_hook_initialized = true;
46        }
47    }
48
49    /**
50     * Refresh stored plan info from WordPress.com when online.
51     *
52     * @return array|WP_Error HTTP response, or an error when offline, unregistered, or the request could not be sent.
53     */
54    public function get_plan_info_from_wpcom() {
55        if ( ( new Status() )->is_offline_mode() ) {
56            return new WP_Error( 'site_offline', 'Site is in offline mode.' );
57        }
58        $blog_id = Jetpack_Options::get_option( 'id' );
59        // An unregistered site (or a stale cache view hiding the registration) has no
60        // blog ID: bail rather than requesting the malformed `/sites//…` path.
61        if ( ! $blog_id ) {
62            return new WP_Error( 'site_not_registered', 'Site not registered.' );
63        }
64        $response = Client::wpcom_json_api_request_as_blog(
65            '/sites/' . $blog_id . '/jetpack-search/plan',
66            '2',
67            array(),
68            null,
69            'wpcom'
70        );
71
72        // store plan in options.
73        $this->update_search_plan_info( $response );
74
75        return $response;
76    }
77
78    /**
79     * Get plan info.
80     *
81     * @param bool $force_refresh - Default to false. Set true to attempt a refresh via get_plan_info_from_wpcom().
82     */
83    public function get_plan_info( $force_refresh = false ) {
84        if ( $force_refresh ) {
85            $this->get_plan_info_from_wpcom();
86        }
87        $plan_info = get_option( self::JETPACK_SEARCH_PLAN_INFO_OPTION_KEY );
88        if ( false === $plan_info && ! $force_refresh ) {
89            $plan_info = $this->get_plan_info( true );
90        }
91        return $plan_info;
92    }
93
94    /**
95     * Please use `supports_instant_search` instead.
96     *
97     * @deprecated
98     */
99    public function has_jetpack_search_product() {
100        return (bool) get_option( 'has_jetpack_search_product' );
101    }
102
103    /**
104     * Returns true if plan supports Instant Search.
105     */
106    public function supports_instant_search() {
107        $plan_info = $this->get_plan_info();
108        return ( isset( $plan_info['supports_instant_search'] ) && $plan_info['supports_instant_search'] );
109    }
110
111    /**
112     * Returns true if the plan support either Instant Search or Classic Search.
113     */
114    public function supports_search() {
115        $plan_info = $this->get_plan_info();
116        return ( isset( $plan_info['supports_search'] ) && $plan_info['supports_search'] );
117    }
118
119    /**
120     * Returns true if the plan usage is exceeded and search should no longer work.
121     */
122    public function must_upgrade() {
123        $plan_info = $this->get_plan_info();
124        return isset( $plan_info['plan_usage']['must_upgrade'] ) && $plan_info['plan_usage']['must_upgrade'];
125    }
126
127    /**
128     * Returns true if the plan only supports Classic Search.
129     */
130    public function supports_only_classic_search() {
131        $plan_info = $this->get_plan_info();
132        return isset( $plan_info['supports_only_classic_search'] ) && $plan_info['supports_only_classic_search'];
133    }
134
135    /**
136     * Whether the plan(s) ever supported search.
137     */
138    public function ever_supported_search() {
139        return (bool) get_option( self::JETPACK_SEARCH_EVER_SUPPORTED_SEARCH ) || $this->supports_search();
140    }
141
142    /**
143     * Returns true if the site is on free plan.
144     */
145    public function is_free_plan() {
146        $plan_info = $this->get_plan_info();
147        return Helper::is_forced_free_plan() || ( isset( $plan_info['effective_subscription']['product_slug'] ) && $plan_info['effective_subscription']['product_slug'] === self::JETPACK_SEARCH_FREE_PRODUCT_SLUG );
148    }
149
150    /**
151     * Update `has_jetpack_search_product` regarding the plan information
152     *
153     * @param array|WP_Error $response - Resopnse from WPCOM.
154     * @return bool - true on success, false on failure.
155     */
156    public function update_search_plan_info( $response ) {
157        if ( is_wp_error( $response ) ) {
158            return false;
159        }
160        $body        = json_decode( wp_remote_retrieve_body( $response ), true );
161        $status_code = wp_remote_retrieve_response_code( $response );
162
163        if ( 200 !== $status_code ) {
164            return false;
165        }
166
167        return $this->set_plan_options( $body );
168    }
169
170    /**
171     * Set plan info to options table
172     *
173     * @param array $plan_info - the decoded plan info array.
174     */
175    public function set_plan_options( $plan_info ) {
176        if ( isset( $plan_info['swap_classic_to_inline_search'] ) && is_bool( $plan_info['swap_classic_to_inline_search'] ) ) {
177            update_option( Module_Control::SEARCH_MODULE_SWAP_CLASSIC_TO_INLINE_OPTION_KEY, (bool) $plan_info['swap_classic_to_inline_search'] );
178        }
179        if ( ! isset( $plan_info['supports_instant_search'] ) ) {
180            return false;
181        }
182        // set option whether has Jetpack Search plan for capability reason.
183        if ( get_option( 'has_jetpack_search_product' ) !== (bool) $plan_info['supports_instant_search'] ) {
184            update_option( 'has_jetpack_search_product', (bool) $plan_info['supports_instant_search'] );
185        }
186        // We use this option to determine the visibility of search submenu.
187        // If the site ever had search subscription, then we record it and show the menu after.
188        if ( $plan_info['supports_instant_search'] ) {
189            update_option( self::JETPACK_SEARCH_EVER_SUPPORTED_SEARCH, true, false );
190        }
191        update_option( self::JETPACK_SEARCH_PLAN_INFO_OPTION_KEY, $plan_info );
192        return true;
193    }
194}