PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-my-jetpack / src / class-red-bubble-notifications.php

class-red-bubble-notifications.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-my-jetpack/src/class-red-bubble-notifications.php

450 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sets up the Red Bubble Notifications rest api endpoint and helper functions
4 *
5 * @package automattic/my-jetpack
6 */
7
8 namespace Automattic\Jetpack\My_Jetpack;
9
10 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11 use Jetpack_Options;
12 use WP_Error;
13 use WP_REST_Request;
14 use WP_REST_Response;
15
16 /**
17 * Registers REST route for getting red bubble notification data
18 * and includes all helper functions related to red bubble notifications
19 */
20 class Red_Bubble_Notifications {
21 private const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
22 private const MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY = 'my-jetpack-red-bubble-transient';
23
24 /**
25 * Summary of register_rest_routes
26 *
27 * @return void
28 */
29 public static function register_rest_endpoints() {
30 register_rest_route(
31 'my-jetpack/v1',
32 'red-bubble-notifications',
33 array(
34 'methods' => \WP_REST_Server::CREATABLE,
35 'callback' => __CLASS__ . '::rest_api_get_red_bubble_alerts',
36 'permission_callback' => __CLASS__ . '::permissions_callback',
37 'args' => array(
38 'dismissal_cookies' => array(
39 'type' => 'array',
40 'description' => 'Array of dismissal cookies to set for the red bubble notifications.',
41 'required' => false,
42 'items' => array(
43 'type' => 'string',
44 ),
45 'sanitize_callback' => function ( $param ) {
46 if ( ! is_array( $param ) ) {
47 return array();
48 }
49 return array_map( 'sanitize_text_field', $param );
50 },
51 ),
52 ),
53 )
54 );
55 }
56
57 /**
58 * Check user capability to access the endpoint.
59 *
60 * @access public
61 * @static
62 *
63 * @return true|WP_Error
64 */
65 public static function permissions_callback() {
66 return current_user_can( 'edit_posts' );
67 }
68
69 /**
70 * Gets the plugins that need installed or activated for each paid plan.
71 *
72 * @return array
73 */
74 public static function get_paid_plans_plugins_requirements() {
75 $plugin_requirements = array();
76 foreach ( Products::get_products_classes() as $slug => $product_class ) {
77 // Skip these- we don't show them in My Jetpack.
78 if ( in_array( $slug, Products::get_not_shown_products(), true ) ) {
79 continue;
80 }
81 // Skip CRM from installation requirements - e.g. don't enforce installation for Complete plan users
82 if ( $slug === 'crm' ) {
83 continue;
84 }
85 if ( ! $product_class::has_paid_plan_for_product() ) {
86 continue;
87 }
88 $purchase = $product_class::get_paid_plan_purchase_for_product();
89 if ( ! $purchase ) {
90 continue;
91 }
92 // Check if required plugin needs installed or activated.
93 if ( ! $product_class::is_plugin_installed() ) {
94 // Plugin needs installed (and activated)
95 $plugin_requirements[ $purchase->product_slug ]['needs_installed'][] = $product_class::$slug;
96 } elseif ( ! $product_class::is_plugin_active() ) {
97 // Plugin is installed, but not activated.
98 $plugin_requirements[ $purchase->product_slug ]['needs_activated_only'][] = $product_class::$slug;
99 }
100 }
101
102 return $plugin_requirements;
103 }
104
105 /**
106 * Check for features broken by a disconnected user or site
107 *
108 * @return array
109 */
110 public static function check_for_broken_modules() {
111 $connection = new Connection_Manager();
112 $is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
113 $is_site_connected = $connection->is_connected();
114 $broken_modules = array(
115 'needs_site_connection' => array(),
116 'needs_user_connection' => array(),
117 );
118
119 if ( $is_user_connected && $is_site_connected ) {
120 return $broken_modules;
121 }
122
123 $products = Products::get_products_classes();
124 $historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
125
126 foreach ( $products as $product ) {
127 if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
128 continue;
129 }
130
131 if ( $product::$requires_user_connection && ! $is_user_connected ) {
132 if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
133 $broken_modules['needs_user_connection'][] = $product::$slug;
134 }
135 } elseif ( ! $is_site_connected ) {
136 if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
137 $broken_modules['needs_site_connection'][] = $product::$slug;
138 }
139 }
140 }
141
142 return $broken_modules;
143 }
144
145 /**
146 * Add an alert slug if the site is missing a site connection, or has no connection owner recorded and the current user can become one.
147 *
148 * @since 6.7.1 Only alerts about a missing user connection when no connection owner is recorded, and to users who can take the vacant owner slot.
149 *
150 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
151 * @return array
152 */
153 public static function alert_if_missing_connection( array $red_bubble_slugs ) {
154 $broken_modules = self::check_for_broken_modules();
155 $connection = new Connection_Manager();
156
157 // Checking for site connection issues first.
158 if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
159 $red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
160 'type' => 'site',
161 'is_error' => true,
162 );
163 return $red_bubble_slugs;
164 }
165
166 // A recorded owner without a token is reported as a connection error instead. Only a user who
167 // can take the vacant owner slot (see Manager::authorize()) fixes a site without an owner.
168 if ( ! empty( $broken_modules['needs_user_connection'] )
169 && ! Jetpack_Options::get_option( 'master_user' )
170 && current_user_can( 'jetpack_connect' ) ) {
171 $red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
172 'type' => 'user',
173 'is_error' => true,
174 );
175 return $red_bubble_slugs;
176 }
177
178 if ( ! $connection->is_connected() ) {
179 $red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
180 'type' => 'site',
181 'is_error' => false,
182 );
183 return $red_bubble_slugs;
184 }
185
186 return $red_bubble_slugs;
187 }
188
189 /**
190 * Add an alert slug if Backups are failing or having an issue.
191 *
192 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
193 * @return array
194 */
195 public static function alert_if_last_backup_failed( array $red_bubble_slugs ) {
196 // Backup is not supported on multisite installations.
197 if ( is_multisite() ) {
198 return $red_bubble_slugs;
199 }
200 // Make sure the Notice wasn't previously dismissed.
201 if ( ! empty( $_COOKIE['backup_failure_dismissed'] ) ) {
202 return $red_bubble_slugs;
203 }
204 // Make sure there's a Backup paid plan
205 if ( ! Products\Backup::is_plugin_active() || ! Products\Backup::has_paid_plan_for_product() ) {
206 return $red_bubble_slugs;
207 }
208 // Make sure the plan isn't just recently purchased in last 30min.
209 // Give some time to queue & run the first backup.
210 $purchase = Products\Backup::get_paid_plan_purchase_for_product();
211 if ( $purchase ) {
212 $thirty_minutes_after_plan_purchase = strtotime( $purchase->subscribed_date . ' +30 minutes' );
213 if ( strtotime( 'now' ) < $thirty_minutes_after_plan_purchase ) {
214 return $red_bubble_slugs;
215 }
216 }
217
218 $backup_failed_status = Products\Backup::does_module_need_attention();
219 if ( $backup_failed_status ) {
220 $red_bubble_slugs['backup_failure'] = $backup_failed_status;
221 }
222
223 return $red_bubble_slugs;
224 }
225
226 /**
227 * Add an alert slug if Protect has scan threats/vulnerabilities.
228 *
229 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
230 * @return array
231 */
232 public static function alert_if_protect_has_threats( array $red_bubble_slugs ) {
233 // Scan is not supported on multisite installations.
234 if ( is_multisite() ) {
235 return $red_bubble_slugs;
236 }
237 // Make sure the Notice hasn't been dismissed.
238 if ( ! empty( $_COOKIE['protect_threats_detected_dismissed'] ) ) {
239 return $red_bubble_slugs;
240 }
241 // Make sure we're dealing with the Protect product only
242 if ( ! Products\Protect::has_paid_plan_for_product() ) {
243 return $red_bubble_slugs;
244 }
245
246 $protect_threats_status = Products\Protect::does_module_need_attention();
247
248 if ( $protect_threats_status ) {
249 $red_bubble_slugs['protect_has_threats'] = $protect_threats_status;
250 }
251
252 return $red_bubble_slugs;
253 }
254
255 /**
256 * Add an alert slug if any paid plan/products are expiring or expired.
257 *
258 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
259 * @return array
260 */
261 public static function alert_if_paid_plan_expiring( array $red_bubble_slugs ) {
262 $connection = new Connection_Manager();
263 if ( ! $connection->is_connected() ) {
264 return $red_bubble_slugs;
265 }
266 $product_classes = Products::get_products_classes();
267
268 $products_included_in_expiring_plan = array();
269 foreach ( $product_classes as $key => $product ) {
270 // Skip these- we don't show them in My Jetpack.
271 if ( in_array( $key, Products::get_not_shown_products(), true ) ) {
272 continue;
273 }
274
275 if ( $product::has_paid_plan_for_product() ) {
276 $purchase = $product::get_paid_plan_purchase_for_product();
277 if ( $purchase ) {
278 // Check if this product is covered by an active bundle plan
279 $is_covered_by_active_bundle = false;
280 if ( ! $product::is_bundle_product() ) {
281 foreach ( $product_classes as $bundle_product ) {
282 if ( $bundle_product::is_bundle_product() &&
283 $bundle_product::has_paid_plan_for_product() &&
284 ! $bundle_product::is_paid_plan_expired() &&
285 ! $bundle_product::is_paid_plan_expiring() &&
286 method_exists( $bundle_product, 'get_supported_products' ) &&
287 in_array( $key, $bundle_product::get_supported_products(), true ) ) {
288 $is_covered_by_active_bundle = true;
289 break;
290 }
291 }
292 }
293
294 // Only show expiration alerts if not covered by an active bundle
295 if ( ! $is_covered_by_active_bundle ) {
296 $redbubble_notice_data = array(
297 'product_slug' => $purchase->product_slug,
298 'product_name' => $purchase->product_name,
299 'expiry_date' => $purchase->expiry_date,
300 'expiry_message' => $purchase->expiry_message,
301 'manage_url' => $product::get_manage_paid_plan_purchase_url(),
302 );
303
304 if ( $product::is_paid_plan_expired() && empty( $_COOKIE[ "$purchase->product_slug--plan_expired_dismissed" ] ) ) {
305 $red_bubble_slugs[ "$purchase->product_slug--plan_expired" ] = $redbubble_notice_data;
306 if ( ! $product::is_bundle_product() ) {
307 $products_included_in_expiring_plan[ "$purchase->product_slug--plan_expired" ][] = $product::get_name();
308 }
309 }
310 if ( $product::is_paid_plan_expiring() && empty( $_COOKIE[ "$purchase->product_slug--plan_expiring_soon_dismissed" ] ) ) {
311 $red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ] = $redbubble_notice_data;
312 $red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ]['manage_url'] = $product::get_renew_paid_plan_purchase_url();
313 if ( ! $product::is_bundle_product() ) {
314 $products_included_in_expiring_plan[ "$purchase->product_slug--plan_expiring_soon" ][] = $product::get_name();
315 }
316 }
317 }
318 }
319 }
320 }
321
322 foreach ( $products_included_in_expiring_plan as $expiring_plan => $products ) {
323 $red_bubble_slugs[ $expiring_plan ]['products_effected'] = $products;
324 }
325
326 return $red_bubble_slugs;
327 }
328
329 /**
330 * Add an alert slug if a site's paid plan requires a plugin install and/or activation.
331 *
332 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
333 * @return array
334 */
335 public static function alert_if_paid_plan_requires_plugin_install_or_activation( array $red_bubble_slugs ) {
336 $connection = new Connection_Manager();
337 // Don't trigger red bubble (and show notice) when the site is not connected or if the
338 // user doesn't have plugin installation/activation permissions.
339 if ( ! $connection->is_connected() || ! current_user_can( 'activate_plugins' ) ) {
340 return $red_bubble_slugs;
341 }
342
343 $plugins_needing_installed_activated = self::get_paid_plans_plugins_requirements();
344 if ( empty( $plugins_needing_installed_activated ) ) {
345 return $red_bubble_slugs;
346 }
347
348 foreach ( $plugins_needing_installed_activated as $plan_slug => $plugins_requirements ) {
349 if ( empty( $_COOKIE[ "$plan_slug--plugins_needing_installed_dismissed" ] ) ) {
350 $red_bubble_slugs[ "$plan_slug--plugins_needing_installed_activated" ] = $plugins_requirements;
351 }
352 }
353
354 return $red_bubble_slugs;
355 }
356
357 /**
358 * Add relevant red bubble notifications
359 *
360 * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
361 * @return array
362 */
363 public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
364 if ( wp_doing_ajax() ) {
365 return array();
366 }
367 $connection = new Connection_Manager();
368 $welcome_banner_dismissed = Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
369 if ( Initializer::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
370 $red_bubble_slugs['welcome-banner-active'] = array(
371 'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
372 );
373 return $red_bubble_slugs;
374 } else {
375 return array_merge(
376 self::alert_if_missing_connection( $red_bubble_slugs ),
377 self::alert_if_last_backup_failed( $red_bubble_slugs ),
378 self::alert_if_paid_plan_expiring( $red_bubble_slugs ),
379 self::alert_if_protect_has_threats( $red_bubble_slugs ),
380 self::alert_if_paid_plan_requires_plugin_install_or_activation( $red_bubble_slugs )
381 );
382 }
383 }
384
385 /**
386 * Get cached red bubble alerts without triggering expensive computation.
387 * Returns the cached transient value or false if not cached.
388 *
389 * @return array|false Cached alerts or false if cache is empty.
390 */
391 public static function get_cached_alerts() {
392 return get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
393 }
394
395 /**
396 * Collect all possible alerts that we might use a red bubble notification for
397 *
398 * @param bool $bypass_cache - whether to bypass the red bubble cache.
399 * @return array
400 */
401 public static function get_red_bubble_alerts( bool $bypass_cache = false ) {
402 static $red_bubble_alerts = array();
403
404 // check for stored alerts
405 $stored_alerts = get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
406
407 // Cache bypass for red bubbles should only happen on the My Jetpack page
408 if ( $stored_alerts !== false && ! ( $bypass_cache ) ) {
409 return $stored_alerts;
410 }
411
412 // go find the alerts
413 $red_bubble_alerts = apply_filters( 'my_jetpack_red_bubble_notification_slugs', $red_bubble_alerts );
414
415 // cache the alerts for one hour
416 set_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY, $red_bubble_alerts, 3600 );
417
418 return $red_bubble_alerts;
419 }
420
421 /**
422 * Get the red bubble alerts, bypassing cache when called via the REST API
423 *
424 * @param WP_REST_Request $request The REST API request object.
425 *
426 * @return WP_Error|WP_REST_Response
427 */
428 public static function rest_api_get_red_bubble_alerts( $request ) {
429 add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
430
431 $cookies = $request->get_param( 'dismissal_cookies' );
432
433 // Update $_COOKIE superglobal with the provided cookies
434 if ( ! empty( $cookies ) && is_array( $cookies ) ) {
435 foreach ( $cookies as $cookie_string ) {
436 // Parse cookie string in format "name=value"
437 $parts = explode( '=', $cookie_string, 2 );
438 if ( count( $parts ) === 2 ) {
439 $name = trim( $parts[0] );
440 $value = trim( $parts[1] );
441 $_COOKIE[ $name ] = $value;
442 }
443 }
444 }
445
446 $red_bubble_alerts = self::get_red_bubble_alerts( true );
447 return rest_ensure_response( $red_bubble_alerts );
448 }
449 }
450