Completed
Push — branch-8.7-built ( 50de0f...7a10d7 )
by Jeremy
32:57 queued 24:29
created

plugin-search.php ➔ jetpack_get_remote_is_psh_active()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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