Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 269
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
jetpack_is_psh_active
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Jetpack_Plugin_Search
0.00% covered (danger)
0.00%
0 / 259
0.00% covered (danger)
0.00%
0 / 20
5402
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 start
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 plugin_details
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 register_endpoints
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 can_request
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_hint_id
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 dismiss
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_dismissed_hints
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 add_to_dismissed_hints
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 should_display_hint
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 load_plugins_search_script
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
2
 get_jetpack_plugin_data
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 get_extra_features
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 inject_jetpack_module_suggestion
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
182
 filter_cards
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 sanitize_search_term
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 by_sorting_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_configure_url
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 insert_module_related_links
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
306
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Plugin Search Hints, aka Feature Suggestions.
4 *
5 * @since 7.1.0
6 *
7 * @package automattic/jetpack
8 */
9
10// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
11
12use Automattic\Jetpack\Constants;
13use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
14use Automattic\Jetpack\Redirect;
15use Automattic\Jetpack\Tracking;
16
17// Disable direct access and execution.
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22if (
23    is_admin() &&
24    Jetpack::is_connection_ready() &&
25    /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
26    apply_filters( 'jetpack_show_promotions', true ) &&
27    // Disable feature hints when plugins cannot be installed.
28    ! Constants::is_true( 'DISALLOW_FILE_MODS' ) &&
29    jetpack_is_psh_active()
30) {
31    Jetpack_Plugin_Search::init();
32}
33
34// Register endpoints when WP REST API is initialized.
35add_action( 'rest_api_init', array( 'Jetpack_Plugin_Search', 'register_endpoints' ) );
36
37/**
38 * Class that includes cards in the plugin search results when users enter terms that match some Jetpack feature.
39 * Card can be dismissed and includes a title, description, button to enable the feature and a link for more information.
40 *
41 * @since 7.1.0
42 */
43class Jetpack_Plugin_Search {
44
45    /**
46     * PSH slug name.
47     *
48     * @var string
49     */
50    public static $slug = 'jetpack-plugin-search';
51
52    /**
53     * Singleton constructor.
54     *
55     * @return Jetpack_Plugin_Search
56     */
57    public static function init() {
58        static $instance = null;
59
60        if ( ! $instance ) {
61            $instance = new Jetpack_Plugin_Search();
62        }
63
64        return $instance;
65    }
66
67    /**
68     * Jetpack_Plugin_Search constructor.
69     */
70    public function __construct() {
71        add_action( 'current_screen', array( $this, 'start' ) );
72    }
73
74    /**
75     * Add actions and filters only if this is the plugin installation screen and it's the first page.
76     *
77     * @param object $screen WP SCreen object.
78     *
79     * @since 7.1.0
80     */
81    public function start( $screen ) {
82        if ( 'plugin-install' === $screen->base && ( ! isset( $_GET['paged'] ) || 1 === intval( $_GET['paged'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
83            add_action( 'admin_enqueue_scripts', array( $this, 'load_plugins_search_script' ) );
84            add_filter( 'plugins_api_result', array( $this, 'inject_jetpack_module_suggestion' ), 10, 3 );
85            add_filter( 'self_admin_url', array( $this, 'plugin_details' ) );
86            add_filter( 'plugin_install_action_links', array( $this, 'insert_module_related_links' ), 10, 2 );
87        }
88    }
89
90    /**
91     * Modify URL used to fetch to plugin information so it pulls Jetpack plugin page.
92     *
93     * @param string $url URL to load in dialog pulling the plugin page from wporg.
94     *
95     * @since 7.1.0
96     *
97     * @return string The URL with 'jetpack' instead of 'jetpack-plugin-search'.
98     */
99    public function plugin_details( $url ) {
100        return false !== stripos( $url, 'tab=plugin-information&amp;plugin=' . self::$slug )
101            ? 'plugin-install.php?tab=plugin-information&amp;plugin=jetpack&amp;TB_iframe=true&amp;width=600&amp;height=550'
102            : $url;
103    }
104
105    /**
106     * Register REST API endpoints.
107     *
108     * @since 7.1.0
109     */
110    public static function register_endpoints() {
111        register_rest_route(
112            'jetpack/v4',
113            '/hints',
114            array(
115                'methods'             => WP_REST_Server::EDITABLE,
116                'callback'            => __CLASS__ . '::dismiss',
117                'permission_callback' => __CLASS__ . '::can_request',
118                'args'                => array(
119                    'hint' => array(
120                        'default'           => '',
121                        'type'              => 'string',
122                        'required'          => true,
123                        'validate_callback' => __CLASS__ . '::is_hint_id',
124                    ),
125                ),
126            )
127        );
128    }
129
130    /**
131     * A WordPress REST API permission callback method that accepts a request object and
132     * decides if the current user has enough privileges to act.
133     *
134     * @since 7.1.0
135     *
136     * @return bool does a current user have enough privileges.
137     */
138    public static function can_request() {
139        return current_user_can( 'jetpack_admin_page' );
140    }
141
142    /**
143     * Validates that the ID of the hint to dismiss is a string.
144     *
145     * @since 7.1.0
146     *
147     * @param string|bool     $value Value to check.
148     * @param WP_REST_Request $request The request sent to the WP REST API.
149     * @param string          $param Name of the parameter passed to endpoint holding $value.
150     *
151     * @return bool|WP_Error
152     */
153    public static function is_hint_id( $value, $request, $param ) {
154        return in_array( $value, Jetpack::get_available_modules(), true )
155            ? true
156            /* translators: %s is the name of a parameter passed to an endpoint. */
157            : new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be an alphanumeric string.', 'jetpack' ), $param ) );
158    }
159
160    /**
161     * A WordPress REST API callback method that accepts a request object and decides what to do with it.
162     *
163     * @param WP_REST_Request $request {
164     *     Array of parameters received by request.
165     *
166     *     @type string $hint Slug of card to dismiss.
167     * }
168     *
169     * @since 7.1.0
170     *
171     * @return bool|array|WP_Error a resulting value or object, or an error.
172     */
173    public static function dismiss( WP_REST_Request $request ) {
174        return self::add_to_dismissed_hints( $request['hint'] )
175            ? rest_ensure_response( array( 'code' => 'success' ) )
176            : new WP_Error( 'not_dismissed', esc_html__( 'The card could not be dismissed', 'jetpack' ), array( 'status' => 400 ) );
177    }
178
179    /**
180     * Returns a list of previously dismissed hints.
181     *
182     * @since 7.1.0
183     *
184     * @return array List of dismissed hints.
185     */
186    protected static function get_dismissed_hints() {
187        $dismissed_hints = Jetpack_Options::get_option( 'dismissed_hints' );
188        return isset( $dismissed_hints ) && is_array( $dismissed_hints )
189            ? $dismissed_hints
190            : array();
191    }
192
193    /**
194     * Save the hint in the list of dismissed hints.
195     *
196     * @since 7.1.0
197     *
198     * @param string $hint The hint id, which is a Jetpack module slug.
199     *
200     * @return bool Whether the card was added to the list and hence dismissed.
201     */
202    protected static function add_to_dismissed_hints( $hint ) {
203        return Jetpack_Options::update_option( 'dismissed_hints', array_merge( self::get_dismissed_hints(), array( $hint ) ) );
204    }
205
206    /**
207     * Checks that the module slug passed should be displayed.
208     *
209     * A feature hint will be displayed if it has not been dismissed before or if 2 or fewer other hints have been dismissed.
210     *
211     * @since 7.2.1
212     *
213     * @param string $hint The hint id, which is a Jetpack module slug.
214     *
215     * @return bool True if $hint should be displayed.
216     */
217    protected function should_display_hint( $hint ) {
218        $dismissed_hints = static::get_dismissed_hints();
219        // If more than 2 hints have been dismissed, then show no more.
220        if ( 2 < count( $dismissed_hints ) ) {
221            return false;
222        }
223
224        $plan = Jetpack_Plan::get();
225        if ( isset( $plan['class'] ) && ( 'free' === $plan['class'] || 'personal' === $plan['class'] ) && 'vaultpress' === $hint ) {
226            return false;
227        }
228
229        return ! in_array( $hint, $dismissed_hints, true );
230    }
231
232    /**
233     * Load the search scripts and CSS for PSH.
234     */
235    public function load_plugins_search_script() {
236        wp_enqueue_script( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION, true );
237        wp_localize_script(
238            self::$slug,
239            'jetpackPluginSearch',
240            array(
241                'nonce'          => wp_create_nonce( 'wp_rest' ),
242                'base_rest_url'  => rest_url( '/jetpack/v4' ),
243                'manageSettings' => esc_html__( 'Configure', 'jetpack' ),
244                'activateModule' => esc_html__( 'Activate Module', 'jetpack' ),
245                'getStarted'     => esc_html__( 'Get started', 'jetpack' ),
246                'activated'      => esc_html__( 'Activated', 'jetpack' ),
247                'activating'     => esc_html__( 'Activating', 'jetpack' ),
248                'logo'           => 'https://ps.w.org/jetpack/assets/icon.svg?rev=1791404',
249                'legend'         => esc_html__(
250                    'This suggestion was made by Jetpack, the security and performance plugin already installed on your site.',
251                    'jetpack'
252                ),
253                'supportText'    => esc_html__(
254                    'Learn more about these suggestions.',
255                    'jetpack'
256                ),
257                'supportLink'    => Redirect::get_url( 'plugin-hint-learn-support' ),
258                'hideText'       => esc_html__( 'Hide this suggestion', 'jetpack' ),
259            )
260        );
261
262        wp_enqueue_style( self::$slug, plugins_url( 'modules/plugin-search/plugin-search.css', JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
263    }
264
265    /**
266     * Get the plugin repo's data for Jetpack to populate the fields with.
267     *
268     * @return array|mixed|object|WP_Error
269     */
270    public static function get_jetpack_plugin_data() {
271        $data = get_transient( 'jetpack_plugin_data' );
272
273        if ( false === $data || is_wp_error( $data ) ) {
274            include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
275            $data = plugins_api(
276                'plugin_information',
277                array(
278                    'slug'   => 'jetpack',
279                    'is_ssl' => is_ssl(),
280                    'fields' => array(
281                        'banners'         => true,
282                        'reviews'         => true,
283                        'active_installs' => true,
284                        'versions'        => false,
285                        'sections'        => false,
286                    ),
287                )
288            );
289            set_transient( 'jetpack_plugin_data', $data, DAY_IN_SECONDS );
290        }
291
292        return $data;
293    }
294
295    /**
296     * Create a list with additional features for those we don't have a module, like Akismet.
297     *
298     * @since 7.1.0
299     *
300     * @return array List of features.
301     */
302    public function get_extra_features() {
303        return array(
304            'akismet'       => array(
305                'name'                => 'Akismet',
306                'search_terms'        => 'akismet, anti-spam, antispam, comments, spam, spam protection, form spam, captcha, no captcha, nocaptcha, recaptcha, phising, google',
307                'short_description'   => esc_html__( 'Keep your visitors and search engines happy by stopping comment and contact form spam with Akismet.', 'jetpack' ),
308                'requires_connection' => true,
309                'module'              => 'akismet',
310                'sort'                => '16',
311                'learn_more_button'   => Redirect::get_url( 'plugin-hint-upgrade-akismet' ),
312                'configure_url'       => admin_url( 'admin.php?page=akismet-key-config' ),
313            ),
314            'sharing-block' => array(
315                'name'                => esc_html__( 'Sharing buttons block', 'jetpack' ),
316                'search_terms'        => 'share, sharing, sharing block, sharing button, social buttons, buttons, share facebook, share twitter, social share, icons, email, facebook, twitter, x, linkedin, pinterest, social media',
317                'short_description'   => esc_html__( 'Add sharing buttons blocks anywhere on your website to help your visitors share your content.', 'jetpack' ),
318                'requires_connection' => false,
319                'module'              => 'sharing-block',
320                'sort'                => '13',
321                'learn_more_button'   => Redirect::get_url( 'jetpack-support-sharing-block' ),
322                'configure_url'       => admin_url( 'site-editor.php?path=%2Fwp_template' ),
323            ),
324        );
325    }
326
327    /**
328     * Intercept the plugins API response and add in an appropriate card for Jetpack
329     *
330     * @param object $result Plugin search results.
331     * @param string $action unused.
332     * @param object $args Search args.
333     */
334    public function inject_jetpack_module_suggestion( $result, $action, $args ) {
335        /*
336         * Bail if something else hooks into the Plugins' API response
337         * and does not return results.
338         */
339        if ( empty( $result->plugins ) || is_wp_error( $result ) ) {
340            return $result;
341        }
342
343        // Looks like a search query; it's matching time.
344        if ( ! empty( $args->search ) ) {
345            $searchable_modules = array(
346                'contact-form',
347                'monitor',
348                'photon',
349                'photon-cdn',
350                'protect',
351                'publicize',
352                'related-posts',
353                'akismet',
354                'vaultpress',
355                'videopress',
356                'search',
357            );
358
359            /*
360             * Let's handle the Sharing feature differently.
361             * If we're using a block-based theme, we should suggest the sharing block.
362             * If using a classic theme, we should suggest the old sharing module.
363             */
364            if ( wp_is_block_theme() ) {
365                $searchable_modules[] = 'sharing-block';
366            } else {
367                $searchable_modules[] = 'sharedaddy';
368            }
369
370            /*
371             * Only surface the SEO Tools hint when the new Jetpack SEO admin page is
372             * available and registered, so its CTA never points to a missing page.
373             */
374            if (
375                \Automattic\Jetpack\SEO\Initializer::is_available()
376                && \Automattic\Jetpack\SEO\Initializer::is_seo_surface_visible()
377            ) {
378                $searchable_modules[] = 'seo-tools';
379            }
380
381            require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
382            $tracking             = new Tracking();
383            $jetpack_modules_list = array_intersect_key(
384                array_merge( $this->get_extra_features(), Jetpack_Admin::init()->get_modules() ),
385                array_flip( $searchable_modules )
386            );
387            uasort( $jetpack_modules_list, array( $this, 'by_sorting_option' ) );
388
389            // Record event when user searches for a term over 3 chars (less than 3 is not very useful).
390            if ( strlen( $args->search ) >= 3 ) {
391                $tracking->record_user_event( 'wpa_plugin_search_term', array( 'search_term' => $args->search ) );
392            }
393
394            // Lowercase, trim, remove punctuation/special chars, decode url, remove 'jetpack'.
395            $normalized_term = $this->sanitize_search_term( $args->search );
396
397            $matching_module = null;
398
399            // Try to match a passed search term with module's search terms.
400            foreach ( $jetpack_modules_list as $module_slug => $module_opts ) {
401                /*
402                * Does the site's current plan support the feature?
403                * We don't use Jetpack_Plan::supports() here because
404                * that check always returns Akismet as supported,
405                * since Akismet has a free version.
406                */
407                $current_plan         = Jetpack_Plan::get();
408                $is_supported_by_plan = in_array( $module_slug, $current_plan['supports'], true );
409
410                if (
411                    false !== stripos( $module_opts['search_terms'] . ', ' . $module_opts['name'], $normalized_term )
412                    && $is_supported_by_plan
413                ) {
414                    $matching_module = $module_slug;
415                    break;
416                }
417            }
418
419            if ( isset( $matching_module ) && $this->should_display_hint( $matching_module ) ) {
420                // Record event when a matching feature is found.
421                $tracking->record_user_event( 'wpa_plugin_search_match_found', array( 'feature' => $matching_module ) );
422
423                $inject    = (array) self::get_jetpack_plugin_data();
424                $image_url = plugins_url( 'modules/plugin-search/psh', JETPACK__PLUGIN_FILE );
425                $overrides = array(
426                    'plugin-search'       => true, // Helps to determine if that an injected card.
427                    'name'                => sprintf(       // Supplement name/description so that they clearly indicate this was added.
428                        /* translators: Jetpack module name */
429                        esc_html_x( 'Jetpack: %s', 'Jetpack: Module Name', 'jetpack' ),
430                        $jetpack_modules_list[ $matching_module ]['name']
431                    ),
432                    'short_description'   => $jetpack_modules_list[ $matching_module ]['short_description'],
433                    'author'              => esc_attr__( 'Jetpack (installed)', 'jetpack' ),
434                    'requires_connection' => (bool) $jetpack_modules_list[ $matching_module ]['requires_connection'],
435                    'slug'                => self::$slug,
436                    'version'             => JETPACK__VERSION,
437                    'icons'               => array(
438                        '1x'  => "$image_url-128.png",
439                        '2x'  => "$image_url-256.png",
440                        'svg' => "$image_url.svg",
441                    ),
442                );
443
444                // Splice in the base module data.
445                $inject = array_merge( $inject, $jetpack_modules_list[ $matching_module ], $overrides );
446
447                // Add it to the top of the list.
448                $result->plugins = array_filter( $result->plugins, array( $this, 'filter_cards' ) );
449                array_unshift( $result->plugins, $inject );
450            }
451        }
452        return $result;
453    }
454
455    /**
456     * Remove cards for Jetpack plugins since we don't want duplicates.
457     *
458     * @since 7.1.0
459     * @since 7.2.0 Only remove Jetpack.
460     * @since 7.4.0 Simplify for WordPress 5.1+.
461     *
462     * @param array|object $plugin WordPress search result card.
463     *
464     * @return bool
465     */
466    public function filter_cards( $plugin ) {
467        /*
468         * $plugin is normally an array.
469         * However, since the response data can be filtered,
470         * we cannot fully trust its format.
471         * Let's handle both arrays and objects, and bail if it's neither.
472         */
473        if ( is_array( $plugin ) && ! empty( $plugin['slug'] ) ) {
474            $slug = $plugin['slug'];
475        } elseif ( is_object( $plugin ) && ! empty( $plugin->slug ) ) {
476            $slug = $plugin->slug;
477        } else {
478            return false;
479        }
480
481        return ! in_array( $slug, array( 'jetpack' ), true );
482    }
483
484    /**
485     * Take a raw search query and return something a bit more standardized and
486     * easy to work with.
487     *
488     * @param  string $term The raw search term.
489     * @return string A simplified/sanitized version.
490     */
491    private function sanitize_search_term( $term ) {
492        $term = strtolower( urldecode( $term ) );
493
494        // remove non-alpha/space chars.
495        $term = preg_replace( '/[^a-z ]/', '', $term );
496
497        // remove strings that don't help matches.
498        $term = trim( str_replace( array( 'jetpack', 'jp', 'free', 'wordpress' ), '', $term ) );
499
500        return $term;
501    }
502
503    /**
504     * Callback function to sort the array of modules by the sort option.
505     *
506     * @param array $m1 Array 1 to sort.
507     * @param array $m2 Array 2 to sort.
508     */
509    private function by_sorting_option( $m1, $m2 ) {
510        return $m1['sort'] <=> $m2['sort'];
511    }
512
513    /**
514     * Modify the URL to the feature settings, for example Publicize.
515     * Sharing is included here because while we still have a page in WP Admin,
516     * we prefer to send users to Calypso.
517     *
518     * @param string $feature Feature.
519     * @param string $configure_url URL to configure feature.
520     *
521     * @return string
522     * @since 7.1.0
523     */
524    private function get_configure_url( $feature, $configure_url ) {
525        switch ( $feature ) {
526            case 'sharing':
527            case 'publicize':
528                $configure_url = Redirect::get_url( 'calypso-marketing-connections' );
529                break;
530            case 'seo-tools':
531                // Jetpack SEO has its own wp-admin page (the new SEO home base);
532                // send users there rather than the legacy Traffic page.
533                $configure_url = admin_url( 'admin.php?page=jetpack-seo' );
534                break;
535            case 'google-analytics':
536                $configure_url = Redirect::get_url(
537                    'calypso-marketing-traffic',
538                    array(
539                        'anchor' => 'analytics',
540                    )
541                );
542                break;
543            case 'wordads':
544                $configure_url = Redirect::get_url( 'wpcom-ads-settings' );
545                break;
546        }
547        return $configure_url;
548    }
549
550    /**
551     * Put some more appropriate links on our custom result cards.
552     *
553     * @param array $links Related links.
554     * @param array $plugin Plugin result information.
555     */
556    public function insert_module_related_links( $links, $plugin ) {
557        if ( self::$slug !== $plugin['slug'] ) {
558            return $links;
559        }
560
561        // By the time this filter is applied, self_admin_url was already applied and we don't need it anymore.
562        remove_filter( 'self_admin_url', array( $this, 'plugin_details' ) );
563
564        $links = array();
565
566        if ( 'sharing-block' === $plugin['module'] ) {
567            $links['jp_get_started'] = '<a
568                id="plugin-select-settings"
569                class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
570                href="' . esc_url( admin_url( 'site-editor.php?path=%2Fwp_template' ) ) . '"
571                data-module="' . esc_attr( $plugin['module'] ) . '"
572                data-track="get_started"
573                >' . esc_html__( 'Add block', 'jetpack' ) . '</a>';
574        } elseif ( 'akismet' === $plugin['module'] || 'vaultpress' === $plugin['module'] ) {
575            $links['jp_get_started'] = '<a
576                id="plugin-select-settings"
577                class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
578                href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '"
579                target="_blank"
580                data-module="' . esc_attr( $plugin['module'] ) . '"
581                data-track="get_started"
582                >' . esc_html__( 'Get started', 'jetpack' ) . '</a>';
583            // Jetpack installed, active, feature not enabled; prompt to enable.
584        } elseif (
585            current_user_can( 'jetpack_activate_modules' ) &&
586            ! Jetpack::is_module_active( $plugin['module'] ) &&
587            Jetpack_Plan::supports( $plugin['module'] )
588        ) {
589            $links[] = '<button
590                    id="plugin-select-activate"
591                    class="jetpack-plugin-search__primary button"
592                    data-module="' . esc_attr( $plugin['module'] ) . '"
593                    data-configure-url="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '"
594                    > ' . esc_html__( 'Enable', 'jetpack' ) . '</button>';
595
596            // Jetpack installed, active, feature enabled; link to settings.
597        } elseif (
598            ! empty( $plugin['configure_url'] ) &&
599            current_user_can( 'jetpack_configure_modules' ) &&
600            Jetpack::is_module_active( $plugin['module'] ) &&
601            /** This filter is documented in class.jetpack-admin.php */
602            apply_filters( 'jetpack_module_configurable_' . $plugin['module'], false )
603        ) {
604            $links[] = '<a
605                id="plugin-select-settings"
606                class="jetpack-plugin-search__primary button jetpack-plugin-search__configure"
607                href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '"
608                data-module="' . esc_attr( $plugin['module'] ) . '"
609                data-track="configure"
610                >' . esc_html__( 'Configure', 'jetpack' ) . '</a>';
611            // Module is active, doesn't have options to configure.
612        } elseif ( 'seo-tools' === $plugin['module'] && Jetpack::is_module_active( $plugin['module'] ) ) {
613            // Jetpack SEO has its own wp-admin page; send users there (same tab)
614            // rather than a jetpack.com doc redirect, which isn't registered for
615            // this module. Reuse get_configure_url() so the destination stays in
616            // one place.
617            $links['jp_get_started'] = '<a
618                id="plugin-select-settings"
619                class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
620                href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '"
621                data-module="' . esc_attr( $plugin['module'] ) . '"
622                data-track="get_started"
623                >' . esc_html__( 'Get started', 'jetpack' ) . '</a>';
624        } elseif ( Jetpack::is_module_active( $plugin['module'] ) ) {
625            $links['jp_get_started'] = '<a
626                id="plugin-select-settings"
627                class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button"
628                href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '"
629                target="_blank"
630                data-module="' . esc_attr( $plugin['module'] ) . '"
631                data-track="get_started"
632                >' . esc_html__( 'Get started', 'jetpack' ) . '</a>';
633        }
634
635        // Add link pointing to a relevant doc page in jetpack.com only if the Get started button isn't displayed.
636        if ( ! empty( $plugin['learn_more_button'] ) && ! isset( $links['jp_get_started'] ) ) {
637            $links[] = '<a
638                class="jetpack-plugin-search__learn-more"
639                href="' . esc_url( $plugin['learn_more_button'] ) . '"
640                target="_blank"
641                data-module="' . esc_attr( $plugin['module'] ) . '"
642                data-track="learn_more"
643                >' . esc_html__( 'Learn more', 'jetpack' ) . '</a>';
644        }
645
646        // Dismiss link.
647        $links[] = '<a
648            class="jetpack-plugin-search__dismiss"
649            data-module="' . esc_attr( $plugin['module'] ) . '"
650            >' . esc_html__( 'Hide this suggestion', 'jetpack' ) . '</a>';
651
652        return $links;
653    }
654}
655
656/**
657 * Master control that checks if Plugin search hints is active.
658 *
659 * @since 7.1.1
660 *
661 * @return bool True if PSH is active.
662 */
663function jetpack_is_psh_active() {
664    /**
665     * Disables the Plugin Search Hints feature found when searching the plugins page.
666     *
667     * @since 8.7.0
668     *
669     * @param bool Set false to disable the feature.
670     */
671    return apply_filters( 'jetpack_psh_active', true );
672}