| @@ -1,0 +1,683 @@ | ||
| 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 | + | |
| 12 | +use Automattic\Jetpack\Constants; | |
| 13 | +use Automattic\Jetpack\Current_Plan as Jetpack_Plan; | |
| 14 | +use Automattic\Jetpack\Redirect; | |
| 15 | +use Automattic\Jetpack\Tracking; | |
| 16 | + | |
| 17 | +// Disable direct access and execution. | |
| 18 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 19 | + exit( 0 ); | |
| 20 | +} | |
| 21 | + | |
| 22 | +if ( | |
| 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. | |
| 35 | +add_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 | + */ | |
| 43 | +class 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&plugin=' . self::$slug ) | |
| 101 | + ? 'plugin-install.php?tab=plugin-information&plugin=jetpack&TB_iframe=true&width=600&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' => $this->get_sharing_block_editor_url(), | |
| 323 | + ), | |
| 324 | + ); | |
| 325 | + } | |
| 326 | + | |
| 327 | + /** | |
| 328 | + * Site Editor URL for the template the Sharing Buttons block goes in. | |
| 329 | + * | |
| 330 | + * @return string | |
| 331 | + */ | |
| 332 | + private function get_sharing_block_editor_url() { | |
| 333 | + return admin_url( | |
| 334 | + 'site-editor.php?p=' . rawurlencode( '/wp_template/' . get_stylesheet() . '//single' ) . '&canvas=edit' | |
| 335 | + ); | |
| 336 | + } | |
| 337 | + | |
| 338 | + /** | |
| 339 | + * Intercept the plugins API response and add in an appropriate card for Jetpack | |
| 340 | + * | |
| 341 | + * @param object $result Plugin search results. | |
| 342 | + * @param string $action unused. | |
| 343 | + * @param object $args Search args. | |
| 344 | + */ | |
| 345 | + public function inject_jetpack_module_suggestion( $result, $action, $args ) { | |
| 346 | + /* | |
| 347 | + * Bail if something else hooks into the Plugins' API response | |
| 348 | + * and does not return results. | |
| 349 | + */ | |
| 350 | + if ( empty( $result->plugins ) || is_wp_error( $result ) ) { | |
| 351 | + return $result; | |
| 352 | + } | |
| 353 | + | |
| 354 | + // Looks like a search query; it's matching time. | |
| 355 | + if ( ! empty( $args->search ) ) { | |
| 356 | + $searchable_modules = array( | |
| 357 | + 'contact-form', | |
| 358 | + 'monitor', | |
| 359 | + 'photon', | |
| 360 | + 'photon-cdn', | |
| 361 | + 'protect', | |
| 362 | + 'publicize', | |
| 363 | + 'related-posts', | |
| 364 | + 'akismet', | |
| 365 | + 'vaultpress', | |
| 366 | + 'videopress', | |
| 367 | + 'search', | |
| 368 | + ); | |
| 369 | + | |
| 370 | + /* | |
| 371 | + * Let's handle the Sharing feature differently. | |
| 372 | + * If we're using a block-based theme, we should suggest the sharing block. | |
| 373 | + * If using a classic theme, we should suggest the old sharing module. | |
| 374 | + */ | |
| 375 | + if ( wp_is_block_theme() ) { | |
| 376 | + $searchable_modules[] = 'sharing-block'; | |
| 377 | + } else { | |
| 378 | + $searchable_modules[] = 'sharedaddy'; | |
| 379 | + } | |
| 380 | + | |
| 381 | + /* | |
| 382 | + * Only surface the SEO Tools hint when the new Jetpack SEO admin page is | |
| 383 | + * available and registered, so its CTA never points to a missing page. | |
| 384 | + */ | |
| 385 | + if ( | |
| 386 | + \Automattic\Jetpack\SEO\Initializer::is_available() | |
| 387 | + && \Automattic\Jetpack\SEO\Initializer::is_seo_surface_visible() | |
| 388 | + ) { | |
| 389 | + $searchable_modules[] = 'seo-tools'; | |
| 390 | + } | |
| 391 | + | |
| 392 | + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; | |
| 393 | + $tracking = new Tracking(); | |
| 394 | + $jetpack_modules_list = array_intersect_key( | |
| 395 | + array_merge( $this->get_extra_features(), Jetpack_Admin::init()->get_modules() ), | |
| 396 | + array_flip( $searchable_modules ) | |
| 397 | + ); | |
| 398 | + uasort( $jetpack_modules_list, array( $this, 'by_sorting_option' ) ); | |
| 399 | + | |
| 400 | + // Record event when user searches for a term over 3 chars (less than 3 is not very useful). | |
| 401 | + if ( strlen( $args->search ) >= 3 ) { | |
| 402 | + $tracking->record_user_event( 'wpa_plugin_search_term', array( 'search_term' => $args->search ) ); | |
| 403 | + } | |
| 404 | + | |
| 405 | + // Lowercase, trim, remove punctuation/special chars, decode url, remove 'jetpack'. | |
| 406 | + $normalized_term = $this->sanitize_search_term( $args->search ); | |
| 407 | + | |
| 408 | + $matching_module = null; | |
| 409 | + | |
| 410 | + // Try to match a passed search term with module's search terms. | |
| 411 | + foreach ( $jetpack_modules_list as $module_slug => $module_opts ) { | |
| 412 | + /* | |
| 413 | + * Does the site's current plan support the feature? | |
| 414 | + * We don't use Jetpack_Plan::supports() here because | |
| 415 | + * that check always returns Akismet as supported, | |
| 416 | + * since Akismet has a free version. | |
| 417 | + */ | |
| 418 | + $current_plan = Jetpack_Plan::get(); | |
| 419 | + $is_supported_by_plan = in_array( $module_slug, $current_plan['supports'], true ); | |
| 420 | + | |
| 421 | + if ( | |
| 422 | + false !== stripos( $module_opts['search_terms'] . ', ' . $module_opts['name'], $normalized_term ) | |
| 423 | + && $is_supported_by_plan | |
| 424 | + ) { | |
| 425 | + $matching_module = $module_slug; | |
| 426 | + break; | |
| 427 | + } | |
| 428 | + } | |
| 429 | + | |
| 430 | + if ( isset( $matching_module ) && $this->should_display_hint( $matching_module ) ) { | |
| 431 | + // Record event when a matching feature is found. | |
| 432 | + $tracking->record_user_event( 'wpa_plugin_search_match_found', array( 'feature' => $matching_module ) ); | |
| 433 | + | |
| 434 | + $inject = (array) self::get_jetpack_plugin_data(); | |
| 435 | + $image_url = plugins_url( 'modules/plugin-search/psh', JETPACK__PLUGIN_FILE ); | |
| 436 | + $overrides = array( | |
| 437 | + 'plugin-search' => true, // Helps to determine if that an injected card. | |
| 438 | + 'name' => sprintf( // Supplement name/description so that they clearly indicate this was added. | |
| 439 | + /* translators: Jetpack module name */ | |
| 440 | + esc_html_x( 'Jetpack: %s', 'Jetpack: Module Name', 'jetpack' ), | |
| 441 | + $jetpack_modules_list[ $matching_module ]['name'] | |
| 442 | + ), | |
| 443 | + 'short_description' => $jetpack_modules_list[ $matching_module ]['short_description'], | |
| 444 | + 'author' => esc_attr__( 'Jetpack (installed)', 'jetpack' ), | |
| 445 | + 'requires_connection' => (bool) $jetpack_modules_list[ $matching_module ]['requires_connection'], | |
| 446 | + 'slug' => self::$slug, | |
| 447 | + 'version' => JETPACK__VERSION, | |
| 448 | + 'icons' => array( | |
| 449 | + '1x' => "$image_url-128.png", | |
| 450 | + '2x' => "$image_url-256.png", | |
| 451 | + 'svg' => "$image_url.svg", | |
| 452 | + ), | |
| 453 | + ); | |
| 454 | + | |
| 455 | + // Splice in the base module data. | |
| 456 | + $inject = array_merge( $inject, $jetpack_modules_list[ $matching_module ], $overrides ); | |
| 457 | + | |
| 458 | + // Add it to the top of the list. | |
| 459 | + $result->plugins = array_filter( $result->plugins, array( $this, 'filter_cards' ) ); | |
| 460 | + array_unshift( $result->plugins, $inject ); | |
| 461 | + } | |
| 462 | + } | |
| 463 | + return $result; | |
| 464 | + } | |
| 465 | + | |
| 466 | + /** | |
| 467 | + * Remove cards for Jetpack plugins since we don't want duplicates. | |
| 468 | + * | |
| 469 | + * @since 7.1.0 | |
| 470 | + * @since 7.2.0 Only remove Jetpack. | |
| 471 | + * @since 7.4.0 Simplify for WordPress 5.1+. | |
| 472 | + * | |
| 473 | + * @param array|object $plugin WordPress search result card. | |
| 474 | + * | |
| 475 | + * @return bool | |
| 476 | + */ | |
| 477 | + public function filter_cards( $plugin ) { | |
| 478 | + /* | |
| 479 | + * $plugin is normally an array. | |
| 480 | + * However, since the response data can be filtered, | |
| 481 | + * we cannot fully trust its format. | |
| 482 | + * Let's handle both arrays and objects, and bail if it's neither. | |
| 483 | + */ | |
| 484 | + if ( is_array( $plugin ) && ! empty( $plugin['slug'] ) ) { | |
| 485 | + $slug = $plugin['slug']; | |
| 486 | + } elseif ( is_object( $plugin ) && ! empty( $plugin->slug ) ) { | |
| 487 | + $slug = $plugin->slug; | |
| 488 | + } else { | |
| 489 | + return false; | |
| 490 | + } | |
| 491 | + | |
| 492 | + return ! in_array( $slug, array( 'jetpack' ), true ); | |
| 493 | + } | |
| 494 | + | |
| 495 | + /** | |
| 496 | + * Take a raw search query and return something a bit more standardized and | |
| 497 | + * easy to work with. | |
| 498 | + * | |
| 499 | + * @param string $term The raw search term. | |
| 500 | + * @return string A simplified/sanitized version. | |
| 501 | + */ | |
| 502 | + private function sanitize_search_term( $term ) { | |
| 503 | + $term = strtolower( urldecode( $term ) ); | |
| 504 | + | |
| 505 | + // remove non-alpha/space chars. | |
| 506 | + $term = preg_replace( '/[^a-z ]/', '', $term ); | |
| 507 | + | |
| 508 | + // remove strings that don't help matches. | |
| 509 | + $term = trim( str_replace( array( 'jetpack', 'jp', 'free', 'wordpress' ), '', $term ) ); | |
| 510 | + | |
| 511 | + return $term; | |
| 512 | + } | |
| 513 | + | |
| 514 | + /** | |
| 515 | + * Callback function to sort the array of modules by the sort option. | |
| 516 | + * | |
| 517 | + * @param array $m1 Array 1 to sort. | |
| 518 | + * @param array $m2 Array 2 to sort. | |
| 519 | + */ | |
| 520 | + private function by_sorting_option( $m1, $m2 ) { | |
| 521 | + return $m1['sort'] <=> $m2['sort']; | |
| 522 | + } | |
| 523 | + | |
| 524 | + /** | |
| 525 | + * Modify the URL to the feature settings, for example Publicize. | |
| 526 | + * Sharing is included here because while we still have a page in WP Admin, | |
| 527 | + * we prefer to send users to Calypso. | |
| 528 | + * | |
| 529 | + * @param string $feature Feature. | |
| 530 | + * @param string $configure_url URL to configure feature. | |
| 531 | + * | |
| 532 | + * @return string | |
| 533 | + * @since 7.1.0 | |
| 534 | + */ | |
| 535 | + private function get_configure_url( $feature, $configure_url ) { | |
| 536 | + switch ( $feature ) { | |
| 537 | + case 'sharing': | |
| 538 | + case 'publicize': | |
| 539 | + $configure_url = Redirect::get_url( 'calypso-marketing-connections' ); | |
| 540 | + break; | |
| 541 | + case 'seo-tools': | |
| 542 | + // Jetpack SEO has its own wp-admin page (the new SEO home base); | |
| 543 | + // send users there rather than the legacy Traffic page. | |
| 544 | + $configure_url = admin_url( 'admin.php?page=jetpack-seo' ); | |
| 545 | + break; | |
| 546 | + case 'google-analytics': | |
| 547 | + $configure_url = Redirect::get_url( | |
| 548 | + 'calypso-marketing-traffic', | |
| 549 | + array( | |
| 550 | + 'anchor' => 'analytics', | |
| 551 | + ) | |
| 552 | + ); | |
| 553 | + break; | |
| 554 | + case 'wordads': | |
| 555 | + $configure_url = Redirect::get_url( 'wpcom-ads-settings' ); | |
| 556 | + break; | |
| 557 | + } | |
| 558 | + return $configure_url; | |
| 559 | + } | |
| 560 | + | |
| 561 | + /** | |
| 562 | + * Put some more appropriate links on our custom result cards. | |
| 563 | + * | |
| 564 | + * @param array $links Related links. | |
| 565 | + * @param array $plugin Plugin result information. | |
| 566 | + */ | |
| 567 | + public function insert_module_related_links( $links, $plugin ) { | |
| 568 | + if ( self::$slug !== $plugin['slug'] ) { | |
| 569 | + return $links; | |
| 570 | + } | |
| 571 | + | |
| 572 | + // By the time this filter is applied, self_admin_url was already applied and we don't need it anymore. | |
| 573 | + remove_filter( 'self_admin_url', array( $this, 'plugin_details' ) ); | |
| 574 | + | |
| 575 | + $links = array(); | |
| 576 | + | |
| 577 | + if ( 'sharing-block' === $plugin['module'] ) { | |
| 578 | + $links['jp_get_started'] = '<a | |
| 579 | + id="plugin-select-settings" | |
| 580 | + class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" | |
| 581 | + href="' . esc_url( $this->get_sharing_block_editor_url() ) . '" | |
| 582 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 583 | + data-track="get_started" | |
| 584 | + >' . esc_html__( 'Add block', 'jetpack' ) . '</a>'; | |
| 585 | + } elseif ( 'akismet' === $plugin['module'] || 'vaultpress' === $plugin['module'] ) { | |
| 586 | + $links['jp_get_started'] = '<a | |
| 587 | + id="plugin-select-settings" | |
| 588 | + class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" | |
| 589 | + href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '" | |
| 590 | + target="_blank" | |
| 591 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 592 | + data-track="get_started" | |
| 593 | + >' . esc_html__( 'Get started', 'jetpack' ) . '</a>'; | |
| 594 | + // Jetpack installed, active, feature not enabled; prompt to enable. | |
| 595 | + } elseif ( | |
| 596 | + current_user_can( 'jetpack_activate_modules' ) && | |
| 597 | + ! Jetpack::is_module_active( $plugin['module'] ) && | |
| 598 | + Jetpack_Plan::supports( $plugin['module'] ) | |
| 599 | + ) { | |
| 600 | + $links[] = '<button | |
| 601 | + id="plugin-select-activate" | |
| 602 | + class="jetpack-plugin-search__primary button" | |
| 603 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 604 | + data-configure-url="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '" | |
| 605 | + > ' . esc_html__( 'Enable', 'jetpack' ) . '</button>'; | |
| 606 | + | |
| 607 | + // Jetpack installed, active, feature enabled; link to settings. | |
| 608 | + } elseif ( | |
| 609 | + ! empty( $plugin['configure_url'] ) && | |
| 610 | + current_user_can( 'jetpack_configure_modules' ) && | |
| 611 | + Jetpack::is_module_active( $plugin['module'] ) && | |
| 612 | + /** This filter is documented in class.jetpack-admin.php */ | |
| 613 | + apply_filters( 'jetpack_module_configurable_' . $plugin['module'], false ) | |
| 614 | + ) { | |
| 615 | + $links[] = '<a | |
| 616 | + id="plugin-select-settings" | |
| 617 | + class="jetpack-plugin-search__primary button jetpack-plugin-search__configure" | |
| 618 | + href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '" | |
| 619 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 620 | + data-track="configure" | |
| 621 | + >' . esc_html__( 'Configure', 'jetpack' ) . '</a>'; | |
| 622 | + // Module is active, doesn't have options to configure. | |
| 623 | + } elseif ( 'seo-tools' === $plugin['module'] && Jetpack::is_module_active( $plugin['module'] ) ) { | |
| 624 | + // Jetpack SEO has its own wp-admin page; send users there (same tab) | |
| 625 | + // rather than a jetpack.com doc redirect, which isn't registered for | |
| 626 | + // this module. Reuse get_configure_url() so the destination stays in | |
| 627 | + // one place. | |
| 628 | + $links['jp_get_started'] = '<a | |
| 629 | + id="plugin-select-settings" | |
| 630 | + class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" | |
| 631 | + href="' . esc_url( $this->get_configure_url( $plugin['module'], $plugin['configure_url'] ) ) . '" | |
| 632 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 633 | + data-track="get_started" | |
| 634 | + >' . esc_html__( 'Get started', 'jetpack' ) . '</a>'; | |
| 635 | + } elseif ( Jetpack::is_module_active( $plugin['module'] ) ) { | |
| 636 | + $links['jp_get_started'] = '<a | |
| 637 | + id="plugin-select-settings" | |
| 638 | + class="jetpack-plugin-search__primary jetpack-plugin-search__get-started button" | |
| 639 | + href="' . esc_url( Redirect::get_url( 'plugin-hint-learn-' . $plugin['module'] ) ) . '" | |
| 640 | + target="_blank" | |
| 641 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 642 | + data-track="get_started" | |
| 643 | + >' . esc_html__( 'Get started', 'jetpack' ) . '</a>'; | |
| 644 | + } | |
| 645 | + | |
| 646 | + // Add link pointing to a relevant doc page in jetpack.com only if the Get started button isn't displayed. | |
| 647 | + if ( ! empty( $plugin['learn_more_button'] ) && ! isset( $links['jp_get_started'] ) ) { | |
| 648 | + $links[] = '<a | |
| 649 | + class="jetpack-plugin-search__learn-more" | |
| 650 | + href="' . esc_url( $plugin['learn_more_button'] ) . '" | |
| 651 | + target="_blank" | |
| 652 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 653 | + data-track="learn_more" | |
| 654 | + >' . esc_html__( 'Learn more', 'jetpack' ) . '</a>'; | |
| 655 | + } | |
| 656 | + | |
| 657 | + // Dismiss link. | |
| 658 | + $links[] = '<a | |
| 659 | + class="jetpack-plugin-search__dismiss" | |
| 660 | + data-module="' . esc_attr( $plugin['module'] ) . '" | |
| 661 | + >' . esc_html__( 'Hide this suggestion', 'jetpack' ) . '</a>'; | |
| 662 | + | |
| 663 | + return $links; | |
| 664 | + } | |
| 665 | +} | |
| 666 | + | |
| 667 | +/** | |
| 668 | + * Master control that checks if Plugin search hints is active. | |
| 669 | + * | |
| 670 | + * @since 7.1.1 | |
| 671 | + * | |
| 672 | + * @return bool True if PSH is active. | |
| 673 | + */ | |
| 674 | +function jetpack_is_psh_active() { | |
| 675 | + /** | |
| 676 | + * Disables the Plugin Search Hints feature found when searching the plugins page. | |
| 677 | + * | |
| 678 | + * @since 8.7.0 | |
| 679 | + * | |
| 680 | + * @param bool Set false to disable the feature. | |
| 681 | + */ | |
| 682 | + return apply_filters( 'jetpack_psh_active', true ); | |
| 683 | +} | |