Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
25.76% covered (danger)
25.76%
17 / 66
9.09% covered (danger)
9.09%
1 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Initializer
25.76% covered (danger)
25.76%
17 / 66
9.09% covered (danger)
9.09%
1 / 11
478.64
0.00% covered (danger)
0.00%
0 / 1
 init
21.74% covered (danger)
21.74%
5 / 23
0.00% covered (danger)
0.00%
0 / 1
38.68
 include_compatibility_files
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 init_before_connection
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 init_search_blocks
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 init_search
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 init_instant_search
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 init_classic_search
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 init_cli
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 jetpack_search_widget_init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_connected
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_search_supported
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initialize
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * Initializer base class.
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\Host;
12use WP_Error;
13/**
14 * Base class for the initializer pattern.
15 */
16class Initializer {
17
18    /**
19     * Whether a block-driven experience owns the search results this request
20     * — Embedded, or the experimental blocks Overlay. Set to `true` in
21     * `init_search_blocks()` only when the `jetpack_search_blocks_enabled`
22     * gate is on AND the saved experience is one of those (the Overlay arm
23     * additionally requires `jetpack_search_overlay_block_template_enabled`).
24     * In those experiences both Classic and Instant Search are suppressed, so
25     * `init_search()` returns falsy by design; `init()` reads this flag to
26     * treat that as a no-op rather than a real failure. Anchoring on the
27     * actually-wired-up state (not a filter read) prevents the abort carve-out
28     * from being bypassed on a site that doesn't have Search Blocks registered.
29     *
30     * @var bool
31     */
32    private static $block_search_active = false;
33
34    /**
35     * Initialize the search package.
36     *
37     * The method is called from the `Config` class.
38     */
39    public static function init() {
40        // Load compatibility files - at this point all plugins are already loaded.
41        static::include_compatibility_files();
42
43        // Set up package version hook.
44        add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package::send_version_to_tracker' );
45
46        /**
47         * The filter allows abortion of the Jetpack Search package initialization.
48         *
49         * @since 0.11.2
50         *
51         * @param boolean $init_search_package Default value is true.
52         */
53        if ( ! apply_filters( 'jetpack_search_init_search_package', true ) ) {
54            /**
55             * Fires when the Jetpack Search fails and would fallback to MySQL.
56             *
57             * @since Jetpack 7.9.0
58             * @param string $reason Reason for Search fallback.
59             * @param mixed  $data   Data associated with the request, such as attempted search parameters.
60             */
61            do_action( 'jetpack_search_abort', 'jetpack_search_init_search_package_filter', null );
62            return;
63        }
64
65        static::init_before_connection();
66
67        // Check whether Jetpack Search should be initialized in the first place .
68        if ( ! static::is_connected() || ! static::is_search_supported() ) {
69            /** This filter is documented in search/src/initalizers/class-initalizer.php */
70            do_action( 'jetpack_search_abort', 'inactive', null );
71            return;
72        }
73
74        // Register the Search 3.0 Interactivity API blocks. Connection +
75        // plan are already guaranteed by the abort above; this call only
76        // layers the Phase 1 feature flag on top, mirroring how
77        // `init_search()` layers `is_instant_search_enabled` on top of
78        // the same upstream gate.
79        static::init_search_blocks();
80
81        $blog_id = Helper::get_wpcom_site_id();
82        if ( ! $blog_id ) {
83            /** This filter is documented in search/src/initalizers/class-initalizer.php */
84            do_action( 'jetpack_search_abort', 'no_blog_id', null );
85            return;
86        }
87
88        if ( ! ( new Module_Control() )->is_active() ) {
89            /** This filter is documented in search/src/initalizers/class-initalizer.php */
90            do_action( 'jetpack_search_abort', 'module_inactive', null );
91            return;
92        }
93
94        // Initialize search package. The block-driven experiences (Embedded /
95        // blocks Overlay) intentionally skip both instant and classic init
96        // (Search_Blocks owns the UI), so a falsy return there is by design —
97        // not an abort. Anything else falsy is a real failure. Anchor on the
98        // actually-wired-up flag (set in `init_search_blocks()` only when the
99        // blocks gate passed) rather than a filter read, so flipping a filter
100        // without the blocks gate can never bypass the abort.
101        $initialized = static::init_search( $blog_id )
102            || self::$block_search_active;
103
104        if ( ! $initialized ) {
105            /** This filter is documented in search/src/initalizers/class-initalizer.php */
106            do_action( 'jetpack_search_abort', 'jetpack_search_init_search', null );
107            return;
108        }
109
110        /**
111         * Fires when the Jetpack Search package has been initialized.
112         *
113         * @since 0.11.2
114         */
115        do_action( 'jetpack_search_loaded' );
116    }
117
118    /**
119     * Extra tweaks to make Jetpack Search play well with others.
120     */
121    public static function include_compatibility_files() {
122        // WordPress.com Simple defines its own unrelated `Jetpack` class, so the class name
123        // alone does not mean the Jetpack plugin, and this shim would fatal there.
124        if ( class_exists( 'Jetpack' ) && ! ( new Host() )->is_wpcom_simple() ) {
125            require_once Package::get_installed_path() . 'compatibility/jetpack.php';
126        }
127        require_once Package::get_installed_path() . 'compatibility/search-0.15.2.php';
128        require_once Package::get_installed_path() . 'compatibility/search-0.17.0.php';
129        require_once Package::get_installed_path() . 'compatibility/unsupported-browsers.php';
130    }
131
132    /**
133     * Init functionality required for connection.
134     */
135    protected static function init_before_connection() {
136        // Set up Search API endpoints.
137        add_action( 'rest_api_init', array( REST_Controller::class, 'register' ) );
138        // The dashboard has to be initialized before connection.
139        ( new Dashboard() )->init_hooks();
140        ( new AI_Answers() )->init();
141    }
142
143    /**
144     * Register the Search 3.0 Interactivity API blocks on this request,
145     * gated by the Phase 1 feature flag.
146     *
147     * Called from `init()` after the upstream connection + Search-plan
148     * abort, so on entry the site is guaranteed to be connected and on a
149     * plan that supports Search (paid plans or the free
150     * `jetpack_search_free` product). The remaining gate is the
151     * feature-flag opt-in.
152     *
153     * Sits before the blog_id and module-active checks because admins
154     * should be able to configure Search blocks in the editor regardless
155     * of which runtime experience is enabled — matching how Instant
156     * Search layers its own opt-in on top of the same connection + plan
157     * gate further down in `init_search()`.
158     */
159    protected static function init_search_blocks() {
160        /**
161         * Filter whether the Jetpack Search 3.0 Interactivity API blocks are enabled.
162         *
163         * Necessary but not sufficient on its own — registration also
164         * requires the site to be connected and on a plan that supports
165         * Search (paid plans or the free `jetpack_search_free` product).
166         *
167         * @param bool $enabled Default true.
168         */
169        if ( ! apply_filters( 'jetpack_search_blocks_enabled', true ) ) {
170            return;
171        }
172
173        Search_Blocks::init();
174
175        // When the Search blocks own the front-end results (Embedded / blocks
176        // Overlay), Classic Search would otherwise run a server-side
177        // Elasticsearch query plus a WP_Query to hydrate the posts on every
178        // search request — work the blocks immediately discard. Suppress it so
179        // it never runs, the same way Instant Search replaces Classic;
180        // `Search_Blocks::filter__posts_pre_query` then short-circuits the
181        // remaining core database search. With both handlers gone `init_search()`
182        // returns false by design, so this flag tells `init()` not to treat that
183        // as an abort.
184        //
185        // Front-end only, matching the `posts_pre_query` registration guard:
186        // leaving Classic Search to initialize normally in wp-admin keeps the
187        // change scoped to the search page and avoids dropping admin-side hooks.
188        if ( ! is_admin() && Search_Blocks::owns_search_results() ) {
189            add_filter( 'jetpack_search_classic_search_enabled', '__return_false' );
190            self::$block_search_active = true;
191        }
192
193        // Experimental block-template overlay (available by default, opt-in
194        // via the Experience Selector; see
195        // `Search_Blocks::is_block_template_overlay_enabled()`): bypass the
196        // preact `SearchApp` so it doesn't race the block overlay for
197        // `?s=`, popstate, and theme search-trigger selectors. Suppressing
198        // at the init filter is cleaner than dequeuing post-enqueue. Gated on
199        // the overlay path specifically — Embedded never enables Instant Search,
200        // so there is nothing to suppress there.
201        if ( Search_Blocks::is_block_template_overlay_enabled() ) {
202            add_filter( 'jetpack_search_init_instant_search', '__return_false' );
203        }
204    }
205
206    /**
207     * Init the search package.
208     *
209     * @param int $blog_id WPCOM blog ID.
210     */
211    protected static function init_search( $blog_id ) {
212        // We could provide CLI to enable search/instant search, so init them regardless of whether the module is active or not.
213        static::init_cli();
214
215        $success                   = false;
216        $is_instant_search_enabled = ( new Module_Control() )->is_instant_search_enabled();
217        if ( $is_instant_search_enabled ) {
218            // Enable Instant search experience.
219            $success = static::init_instant_search( $blog_id );
220        }
221        /**
222         * Filter whether classic search should be enabled. By this stage, search module would be enabled already.
223         *
224         * @since 0.39.6
225         * @param boolean initial value whether classic search is enabled.
226         * @param boolean filtered result whether classic search is enabled.
227         */
228        if ( apply_filters( 'jetpack_search_classic_search_enabled', ! $is_instant_search_enabled ) ) {
229            // Enable the classic search experience.
230            $success = static::init_classic_search( $blog_id );
231        }
232
233        if ( $success ) {
234            // registers Jetpack Search widget.
235            add_action( 'widgets_init', array( static::class, 'jetpack_search_widget_init' ) );
236        }
237
238        return $success;
239    }
240
241    /**
242     * Init Instant Search and its dependencies.
243     *
244     * @param int $blog_id WPCOM blog ID.
245     */
246    protected static function init_instant_search( $blog_id ) {
247        /**
248         * The filter allows abortion of the Instant Search initialization.
249         *
250         * @since 0.11.2
251         *
252         * @param boolean $init_instant_search Default value is true.
253         */
254        if ( ! apply_filters( 'jetpack_search_init_instant_search', true ) ) {
255            return;
256        }
257
258        // Enable the instant search experience.
259        Instant_Search::initialize( $blog_id );
260        // Register instant search configurables as WordPress settings.
261        new Settings();
262        // Instantiate "Customberg", the live search configuration interface.
263        Customberg::instance();
264        // Enable configuring instant search within the Customizer iff it's not using a block theme.
265        if ( ! wp_is_block_theme() ) {
266            new Customizer();
267        }
268        return true;
269    }
270
271    /**
272     * Init Classic Search.
273     *
274     * @param int $blog_id WPCOM blog ID.
275     */
276    protected static function init_classic_search( $blog_id ) {
277        /**
278         * The filter allows abortion of the Classic Search initialization.
279         *
280         * @since 0.11.2
281         *
282         * @param boolean $init_instant_search Default value is true.
283         */
284        if ( ! apply_filters( 'jetpack_search_init_classic_search', true ) ) {
285            return;
286        }
287        Inline_Search::get_instance_maybe_fallback_to_classic( $blog_id );
288
289        return true;
290    }
291
292    /**
293     * Register jetpack-search CLI if `\CLI` exists.
294     *
295     * @return void
296     */
297    protected static function init_cli() {
298        if ( defined( 'WP_CLI' ) && \WP_CLI ) {
299            \WP_CLI::add_command( 'jetpack-search', __NAMESPACE__ . '\CLI' );
300        }
301    }
302
303    /**
304     * Register the widget if Jetpack Search is available and enabled.
305     */
306    public static function jetpack_search_widget_init() {
307        register_widget( 'Automattic\Jetpack\Search\Search_Widget' );
308    }
309
310    /**
311     * Check if site has been connected.
312     */
313    protected static function is_connected() {
314        return ( new Connection_Manager( Package::SLUG ) )->is_connected();
315    }
316
317    /**
318     * Check if search is supported by current plan.
319     */
320    protected static function is_search_supported() {
321        return ( new Plan() )->supports_search();
322    }
323
324    /**
325     * Perform necessary initialization steps for classic and instant search in the constructor.
326     *
327     * @deprecated
328     */
329    public static function initialize() {
330        return new WP_Error(
331            'invalid-method',
332            /* translators: %s: Method name. */
333            sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'jetpack-search-pkg' ), __METHOD__ ),
334            array( 'status' => 405 )
335        );
336    }
337}