暂无描述

plugin-search.php 20KB

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