PluginProbe
WooCommerce / 11.1.0-beta.1
WooCommerce v11.1.0-beta.1
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Admin / PluginsHelper.php
PluginsHelper.php
1,284 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * PluginsHelper
4 *
5 * Helper class for the site's plugins.
6 */
7
8 namespace Automattic\WooCommerce\Admin;
9
10 use ActionScheduler;
11 use ActionScheduler_DBStore;
12 use ActionScheduler_QueueRunner;
13 use Automatic_Upgrader_Skin;
14 use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsyncPluginsInstallLogger;
15 use Automattic\WooCommerce\Admin\PluginsInstallLoggers\PluginsInstallLogger;
16 use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
17 use Automattic\WooCommerce\Utilities\PluginUtil;
18 use Plugin_Upgrader;
19 use WC_Helper;
20 use WC_Helper_Updater;
21 use WP_Error;
22 use WP_Upgrader;
23
24 defined( 'ABSPATH' ) || exit;
25
26 if ( ! function_exists( 'get_plugins' ) ) {
27 require_once ABSPATH . 'wp-admin/includes/plugin.php';
28 }
29
30 /**
31 * Class PluginsHelper
32 */
33 class PluginsHelper {
34
35 /**
36 * Subscription notices in Woo screens are shown in clear priority order, first
37 * expired, and if those don't exist, expiring, and finally if none of those exist,
38 * then missing. This keeps track of whether we can show the next set of notices.
39 *
40 * @var bool
41 */
42 public static $subscription_usage_notices_already_shown = false;
43
44 /**
45 * The URL for the WooCommerce subscription page.
46 */
47 const WOO_SUBSCRIPTION_PAGE_URL = 'https://woocommerce.com/my-account/my-subscriptions/';
48
49 /**
50 * The URL for the WooCommerce.com cart page.
51 */
52 const WOO_CART_PAGE_URL = 'https://woocommerce.com/cart/';
53
54 /**
55 * The URL for the WooCommerce.com add payment method page.
56 */
57 const WOO_ADD_PAYMENT_METHOD_URL = 'https://woocommerce.com/my-account/add-payment-method/';
58
59 /**
60 * Meta key for dismissing expired subscription notices.
61 */
62 const DISMISS_EXPIRED_SUBS_NOTICE = 'woo_subscription_expired_notice_dismiss';
63
64 /**
65 * Meta key for dismissing expiring subscription notices
66 */
67 const DISMISS_EXPIRING_SUBS_NOTICE = 'woo_subscription_expiring_notice_dismiss';
68
69 /**
70 * Meta key for dismissing missing subscription notices
71 */
72 const DISMISS_MISSING_SUBS_NOTICE = 'woo_subscription_missing_notice_dismiss';
73
74 /**
75 * Meta key for dismissing disconnected notice
76 */
77 const DISMISS_DISCONNECT_NOTICE = 'woo_disconnect_notice_dismiss';
78
79 /**
80 * Meta key for dismissing connected notice
81 */
82 const DISMISS_CONNECT_NOTICE = 'woo_connect_notice_dismiss';
83
84 /**
85 * Initialize hooks.
86 */
87 public static function init() {
88 add_action( 'woocommerce_plugins_install_callback', array( __CLASS__, 'install_plugins' ), 10, 2 );
89 add_action( 'woocommerce_plugins_install_and_activate_async_callback', array( __CLASS__, 'install_and_activate_plugins_async_callback' ), 10, 3 );
90 add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 );
91 add_action( 'admin_notices', array( __CLASS__, 'maybe_show_connect_notice_in_plugin_list' ) );
92 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice' ) );
93 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_notices_in_plugins' ) );
94 }
95
96 /**
97 * Get the path to the plugin file relative to the plugins directory from the plugin slug.
98 *
99 * E.g. 'woocommerce' returns 'woocommerce/woocommerce.php'
100 *
101 * @param string $slug Plugin slug to get path for.
102 *
103 * @return string|false The plugin path or false if the plugin is not installed.
104 */
105 public static function get_plugin_path_from_slug( $slug ) {
106 $plugins = get_plugins();
107
108 if ( strstr( $slug, '/' ) ) {
109 // The slug is already a plugin path.
110 return $slug;
111 }
112
113 foreach ( $plugins as $plugin_path => $data ) {
114 $path_parts = explode( '/', $plugin_path );
115 if ( $path_parts[0] === $slug ) {
116 return $plugin_path;
117 }
118 }
119
120 return false;
121 }
122
123 /**
124 * Get an array of installed plugin slugs.
125 *
126 * @return array
127 */
128 public static function get_installed_plugin_slugs() {
129 return array_map(
130 function ( $plugin_path ) {
131 $path_parts = explode( '/', $plugin_path );
132
133 return $path_parts[0];
134 },
135 array_keys( get_plugins() )
136 );
137 }
138
139 /**
140 * Get an array of installed plugins with their file paths as a key value pair.
141 *
142 * @return array
143 */
144 public static function get_installed_plugins_paths() {
145 $plugins = get_plugins();
146 $installed_plugins = array();
147
148 foreach ( $plugins as $path => $plugin ) {
149 $path_parts = explode( '/', $path );
150 $slug = $path_parts[0];
151 $installed_plugins[ $slug ] = $path;
152 }
153
154 return $installed_plugins;
155 }
156
157 /**
158 * Get an array of active plugin slugs.
159 *
160 * The list will include both network active and site active plugins.
161 *
162 * @return array The list of active plugin slugs.
163 */
164 public static function get_active_plugin_slugs() {
165 return array_unique(
166 array_map(
167 function ( $absolute_path ) {
168 // Make the path relative to the plugins directory.
169 $plugin_path = str_replace( WP_PLUGIN_DIR . '/', '', $absolute_path );
170
171 // Split the path to get the plugin slug (aka the directory name).
172 $path_parts = explode( '/', $plugin_path );
173
174 return $path_parts[0];
175 },
176 // Use this method as it is the most bulletproof way to get the active plugins.
177 wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins()
178 )
179 );
180 }
181
182 /**
183 * Checks if a plugin is installed.
184 *
185 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
186 *
187 * @return bool
188 */
189 public static function is_plugin_installed( $plugin ) {
190 $plugin_path = self::get_plugin_path_from_slug( $plugin );
191
192 return $plugin_path ? array_key_exists( $plugin_path, get_plugins() ) : false;
193 }
194
195 /**
196 * Checks if a plugin is active.
197 *
198 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
199 *
200 * @return bool
201 */
202 public static function is_plugin_active( $plugin ) {
203 $plugin_path = self::get_plugin_path_from_slug( $plugin );
204
205 return $plugin_path && \is_plugin_active( $plugin_path );
206 }
207
208 /**
209 * Get plugin data.
210 *
211 * @param string $plugin Path to the plugin file relative to the plugins directory or the plugin directory name.
212 *
213 * @return array|false
214 */
215 public static function get_plugin_data( $plugin ) {
216 $plugin_path = self::get_plugin_path_from_slug( $plugin );
217 $plugins = get_plugins();
218
219 return isset( $plugins[ $plugin_path ] ) ? $plugins[ $plugin_path ] : false;
220 }
221
222 /**
223 * Install an array of plugins.
224 *
225 * @param array $plugins Plugins to install.
226 * @param PluginsInstallLogger|null $logger an optional logger.
227 * @param string|null $source place where the request is coming from.
228 *
229 * @return array
230 */
231 public static function install_plugins( $plugins, ?PluginsInstallLogger $logger = null, ?string $source = null ) {
232 /**
233 * Filter the list of plugins to install.
234 *
235 * @param array $plugins A list of the plugins to install.
236 *
237 * @since 6.4.0
238 */
239 $plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins );
240
241 if ( empty( $plugins ) || ! is_array( $plugins ) ) {
242 return new WP_Error(
243 'woocommerce_plugins_invalid_plugins',
244 __( 'Plugins must be a non-empty array.', 'woocommerce' )
245 );
246 }
247
248 require_once ABSPATH . 'wp-admin/includes/plugin.php';
249 include_once ABSPATH . '/wp-admin/includes/admin.php';
250 include_once ABSPATH . '/wp-admin/includes/plugin-install.php';
251 include_once ABSPATH . '/wp-admin/includes/plugin.php';
252 include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
253 include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
254
255 $existing_plugins = self::get_installed_plugins_paths();
256 $installed_plugins = array();
257 $results = array();
258 $time = array();
259 $errors = new WP_Error();
260 $install_start_time = time();
261
262 foreach ( $plugins as $plugin ) {
263 $slug = sanitize_key( $plugin );
264 $logger && $logger->install_requested( $plugin );
265
266 if ( isset( $existing_plugins[ $slug ] ) ) {
267 $installed_plugins[] = $plugin;
268 $logger && $logger->installed( $plugin, 0 );
269 continue;
270 }
271
272 $start_time = microtime( true );
273
274 $api = plugins_api(
275 'plugin_information',
276 array(
277 'slug' => $slug,
278 'fields' => array(
279 'sections' => false,
280 ),
281 )
282 );
283
284 if ( is_wp_error( $api ) ) {
285 $properties = array(
286 'error_message' => sprintf(
287 // translators: %s: plugin slug (example: woocommerce-services).
288 __(
289 'We couldn\'t install `%s`. Try again in a few minutes, or install it later from the Extensions page.',
290 'woocommerce'
291 ),
292 $slug
293 ),
294 'api_error_message' => $api->get_error_message(),
295 'slug' => $slug,
296 );
297 wc_admin_record_tracks_event( 'install_plugin_error', $properties );
298
299 /**
300 * Action triggered when a plugin API call failed.
301 *
302 * @param string $slug The plugin slug.
303 * @param WP_Error $api The API response.
304 *
305 * @since 6.4.0
306 */
307 do_action( 'woocommerce_plugins_install_api_error', $slug, $api );
308
309 $error_message = sprintf(
310 /* translators: %s: plugin slug (example: woocommerce-services) */
311 __( 'We couldn\'t install `%s`. Try again in a few minutes, or install it later from the Extensions page.', 'woocommerce' ),
312 $slug
313 );
314
315 $errors->add( $plugin, $error_message );
316 $logger && $logger->add_error( $plugin, $error_message );
317
318 continue;
319 }
320
321 /**
322 * Action triggered before a plugin is installed.
323 *
324 * @since 9.8
325 */
326 do_action( 'woocommerce_plugins_install_before', $slug, $source );
327
328 $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
329 $result = $upgrader->install( $api->download_link );
330 // result can be false or WP_Error.
331 $results[ $plugin ] = $result;
332 $time[ $plugin ] = round( ( microtime( true ) - $start_time ) * 1000 );
333
334 if ( is_wp_error( $result ) || is_null( $result ) ) {
335 $properties = array(
336 'error_message' => sprintf(
337 /* translators: %s: plugin slug (example: woocommerce-services) */
338 __(
339 'The requested plugin `%s` could not be installed.',
340 'woocommerce'
341 ),
342 $slug
343 ),
344 'slug' => $slug,
345 'api_version' => $api->version,
346 'api_download_link' => $api->download_link,
347 'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ),
348 'result' => is_wp_error( $result ) ? $result->get_error_message() : 'null',
349 );
350 wc_admin_record_tracks_event( 'install_plugin_error', $properties );
351
352 /**
353 * Action triggered when a plugin installation fails.
354 *
355 * @param string $slug The plugin slug.
356 * @param object $api The plugin API object.
357 * @param WP_Error|null $result The result of the plugin installation.
358 * @param Plugin_Upgrader $upgrader The plugin upgrader.
359 *
360 * @since 6.4.0
361 */
362 do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader );
363
364 $install_error_message = sprintf(
365 /* translators: %s: plugin slug (example: woocommerce-services) */
366 __( 'We couldn\'t install `%s`. Try again, or install it manually. If it keeps failing, contact your host.', 'woocommerce' ),
367 $slug
368 );
369 $errors->add(
370 $plugin,
371 $install_error_message
372 );
373 $logger && $logger->add_error( $plugin, $install_error_message );
374
375 continue;
376 }
377
378 $installed_plugins[] = $plugin;
379 $logger && $logger->installed( $plugin, $time[ $plugin ] );
380
381 /**
382 * Action triggered after a plugin is installed.
383 *
384 * @since 9.8
385 */
386 do_action( 'woocommerce_plugins_install_after', $slug, $source );
387 }
388
389 $data = array(
390 'installed' => $installed_plugins,
391 'results' => $results,
392 'errors' => $errors,
393 'time' => $time,
394 );
395
396 $logger && $logger->complete( array_merge( $data, array( 'start_time' => $install_start_time ) ) );
397
398 return $data;
399 }
400
401 /**
402 * Callback registered by OnboardingPlugins::install_and_activate_async.
403 *
404 * It is used to call install_plugins and activate_plugins with a custom logger.
405 *
406 * @param array $plugins A list of plugins to install.
407 * @param string $job_id An unique job I.D.
408 * @param string|null $source The source of the request.
409 *
410 * @return bool
411 */
412 public static function install_and_activate_plugins_async_callback( array $plugins, string $job_id, ?string $source = null ) {
413 $option_name = 'woocommerce_onboarding_plugins_install_and_activate_async_' . $job_id;
414 $logger = new AsyncPluginsInstallLogger( $option_name );
415 self::install_plugins( $plugins, $logger, $source );
416 self::activate_plugins( $plugins, $logger );
417 return true;
418 }
419
420 /**
421 * Schedule plugin installation.
422 *
423 * @param array $plugins Plugins to install.
424 *
425 * @return string Job ID.
426 */
427 public static function schedule_install_plugins( $plugins ) {
428 if ( empty( $plugins ) || ! is_array( $plugins ) ) {
429 return new WP_Error(
430 'woocommerce_plugins_invalid_plugins',
431 __( 'Plugins must be a non-empty array.', 'woocommerce' ),
432 404
433 );
434 }
435
436 $job_id = uniqid();
437 WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_install_callback', array( $plugins ) );
438
439 return $job_id;
440 }
441
442 /**
443 * Activate the requested plugins.
444 *
445 * @param array $plugins Plugins.
446 * @param PluginsInstallLogger|null $logger Logger.
447 *
448 * @return WP_Error|array Plugin Status
449 */
450 public static function activate_plugins( $plugins, ?PluginsInstallLogger $logger = null ) {
451 if ( empty( $plugins ) || ! is_array( $plugins ) ) {
452 return new WP_Error(
453 'woocommerce_plugins_invalid_plugins',
454 __( 'Plugins must be a non-empty array.', 'woocommerce' ),
455 404
456 );
457 }
458
459 require_once ABSPATH . 'wp-admin/includes/plugin.php';
460
461 // the mollie-payments-for-woocommerce plugin calls `WP_Filesystem()` during it's activation hook, which crashes without this include.
462 require_once ABSPATH . 'wp-admin/includes/file.php';
463
464 /**
465 * Filter the list of plugins to activate.
466 *
467 * @param array $plugins A list of the plugins to activate.
468 *
469 * @since 6.4.0
470 */
471 $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins );
472
473 $plugin_paths = self::get_installed_plugins_paths();
474 $errors = new WP_Error();
475 $activated_plugins = array();
476
477 foreach ( $plugins as $plugin ) {
478 $slug = $plugin;
479 $path = isset( $plugin_paths[ $slug ] ) ? $plugin_paths[ $slug ] : false;
480
481 if ( ! $path ) {
482 /* translators: %s: plugin slug (example: woocommerce-services) */
483 $message = sprintf( __( 'The requested plugin `%s`. is not yet installed.', 'woocommerce' ), $slug );
484 $errors->add(
485 $plugin,
486 $message
487 );
488 $logger && $logger->add_error( $plugin, $message );
489 continue;
490 }
491
492 $result = activate_plugin( $path );
493 if ( ! is_plugin_active( $path ) ) {
494 /**
495 * Action triggered when a plugin activation fails.
496 *
497 * @param string $slug The plugin slug.
498 * @param null|WP_Error $result The result of the plugin activation.
499 *
500 * @since 6.4.0
501 */
502 do_action( 'woocommerce_plugins_activate_error', $slug, $result );
503
504 /* translators: %s: plugin slug (example: woocommerce-services) */
505 $message = sprintf( __( 'The requested plugin `%s` could not be activated.', 'woocommerce' ), $slug );
506 $errors->add(
507 $plugin,
508 $message
509 );
510 $logger && $logger->add_error( $plugin, $message );
511
512 continue;
513 }
514
515 $activated_plugins[] = $plugin;
516 $logger && $logger->activated( $plugin );
517 }
518
519 $data = array(
520 'activated' => $activated_plugins,
521 'active' => self::get_active_plugin_slugs(),
522 'errors' => $errors,
523 );
524
525 return $data;
526 }
527
528 /**
529 * Schedule plugin activation.
530 *
531 * @param array $plugins Plugins to activate.
532 *
533 * @return string Job ID.
534 */
535 public static function schedule_activate_plugins( $plugins ) {
536 if ( empty( $plugins ) || ! is_array( $plugins ) ) {
537 return new WP_Error(
538 'woocommerce_plugins_invalid_plugins',
539 __( 'Plugins must be a non-empty array.', 'woocommerce' ),
540 404
541 );
542 }
543
544 $job_id = uniqid();
545 WC()->queue()->schedule_single(
546 time() + 5,
547 'woocommerce_plugins_activate_callback',
548 array( $plugins, $job_id )
549 );
550
551 return $job_id;
552 }
553
554 /**
555 * Installation status.
556 *
557 * @param int $job_id Job ID.
558 *
559 * @return array Job data.
560 */
561 public static function get_installation_status( $job_id = null ) {
562 $actions = WC()->queue()->search(
563 array(
564 'hook' => 'woocommerce_plugins_install_callback',
565 'search' => $job_id,
566 'orderby' => 'date',
567 'order' => 'DESC',
568 )
569 );
570
571 return self::get_action_data( $actions );
572 }
573
574 /**
575 * Gets the plugin data for the first action.
576 *
577 * @param array $actions Array of AS actions.
578 *
579 * @return array Array of action data.
580 */
581 public static function get_action_data( $actions ) {
582 $data = array();
583
584 foreach ( $actions as $action_id => $action ) {
585 $store = new ActionScheduler_DBStore();
586 $args = $action->get_args();
587 $data[] = array(
588 'job_id' => $args[1],
589 'plugins' => $args[0],
590 'status' => $store->get_status( $action_id ),
591 );
592 }
593
594 return $data;
595 }
596
597 /**
598 * Activation status.
599 *
600 * @param int $job_id Job ID.
601 *
602 * @return array Array of action data.
603 */
604 public static function get_activation_status( $job_id = null ) {
605 $actions = WC()->queue()->search(
606 array(
607 'hook' => 'woocommerce_plugins_activate_callback',
608 'search' => $job_id,
609 'orderby' => 'date',
610 'order' => 'DESC',
611 )
612 );
613
614 return self::get_action_data( $actions );
615 }
616
617 /**
618 * Show notices to connect to woocommerce.com for unconnected store in the plugin list.
619 *
620 * @return void
621 */
622 public static function maybe_show_connect_notice_in_plugin_list() {
623 if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
624 return;
625 }
626
627 $notice_type = WC_Helper_Updater::get_woo_connect_notice_type();
628
629 // The outdated plugin risk state is reported in Site Health.
630 if ( in_array( $notice_type, array( 'none', 'long' ), true ) ) {
631 return;
632 }
633
634 $connect_page_url = add_query_arg(
635 array(
636 'page' => 'wc-admin',
637 'tab' => 'my-subscriptions',
638 'path' => rawurlencode( '/extensions' ),
639 'utm_source' => 'pu',
640 'utm_campaign' => 'pu_setting_screen_connect',
641 ),
642 admin_url( 'admin.php' )
643 );
644
645 $notice_string = sprintf(
646 /* translators: %s: Connect page URL */
647 __( '<a id="woo-connect-notice-url" href="%s">Connect your store</a> to WooCommerce.com to get updates and streamlined support for your subscriptions.', 'woocommerce' ),
648 esc_url( $connect_page_url )
649 );
650
651 echo '<div class="woo-connect-notice notice notice-error is-dismissible">
652 <p class="widefat">' . wp_kses_post( $notice_string ) . '</p>
653 </div>';
654 }
655
656 /**
657 * Enqueue scripts for connect notice in WooCommerce settings page.
658 *
659 * @return void
660 */
661 public static function maybe_enqueue_scripts_for_connect_notice() {
662 if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
663 return;
664 }
665
666 $notice_type = WC_Helper_Updater::get_woo_connect_notice_type();
667
668 if ( in_array( $notice_type, array( 'none', 'long' ), true ) ) {
669 return;
670 }
671
672 WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-connect-notice' );
673 wp_enqueue_script( 'woo-connect-notice' );
674 }
675
676 /**
677 * Enqueue scripts for notices in plugin list page.
678 *
679 * @return void
680 */
681 public static function maybe_enqueue_scripts_for_notices_in_plugins() {
682 if ( 'plugins' !== get_current_screen()->id ) {
683 return;
684 }
685
686 WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-plugin-update-connect-notice' );
687 WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-enable-autorenew' );
688 WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-renew-subscription' );
689 wp_enqueue_script( 'woo-plugin-update-connect-notice' );
690 wp_enqueue_script( 'woo-enable-autorenew' );
691 wp_enqueue_script( 'woo-renew-subscription' );
692 wp_enqueue_script( 'woo-purchase-subscription' );
693 }
694
695 /**
696 * Show notice about to expired subscription on WC settings page.
697 *
698 * @return void
699 */
700 public static function maybe_show_expired_subscriptions_notice() {
701
702 if ( ! WC_Helper::is_site_connected() ) {
703 return;
704 }
705
706 if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
707 return;
708 }
709
710 $notice = self::get_expired_subscription_notice();
711
712 if ( isset( $notice['description'] ) ) {
713 echo '<div id="woo-subscription-expired-notice" class="woo-subscription-expired-notice woo-subscription-notices notice notice-error is-dismissible" data-dismissnonce="' . esc_attr( wp_create_nonce( 'dismiss_notice' ) ) . '">
714 <p class="widefat">' . wp_kses_post( $notice['description'] ) . '</p>
715 </div>';
716 }
717 }
718
719 /**
720 * Show notice about to expiring subscription on WC settings page.
721 *
722 * @return void
723 */
724 public static function maybe_show_expiring_subscriptions_notice() {
725 if ( ! WC_Helper::is_site_connected() ) {
726 return;
727 }
728
729 if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
730 return;
731 }
732
733 $notice = self::get_expiring_subscription_notice();
734
735 if ( isset( $notice['description'] ) ) {
736 echo '<div id="woo-subscription-expiring-notice" class="woo-subscription-expiring-notice woo-subscription-notices notice notice-error is-dismissible" data-dismissnonce="' . esc_attr( wp_create_nonce( 'dismiss_notice' ) ) . '">
737 <p class="widefat">' . wp_kses_post( $notice['description'] ) . '</p>
738 </div>';
739 }
740 }
741
742 /**
743 * Enqueue scripts for woo subscription notice.
744 *
745 * @return void
746 */
747 public static function maybe_enqueue_scripts_for_subscription_notice() {
748 if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
749 return;
750 }
751
752 WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-subscriptions-notice' );
753 wp_enqueue_script( 'woo-subscriptions-notice' );
754 }
755
756 /**
757 * Construct the subscription notice data based on user subscriptions data.
758 *
759 * @param array $all_subs all subscription data.
760 * @param array $subs_to_show filtered subscriptions as condition.
761 * @param int $total total subscription count.
762 * @param array $messages message.
763 * @param string $type type of notice, whether it is for expiring or expired subscription.
764 * @return array notice data to return. Contains type, parsed_message and product_id (can be a single value or an array).
765 */
766 public static function get_subscriptions_notice_data( array $all_subs, array $subs_to_show, int $total, array $messages, string $type ) {
767 $utm_campaign = 'expired' === $type ?
768 'pu_settings_screen_renew' :
769 ( 'missing' === $type ? 'pu_settings_screen_purchase' : 'pu_settings_screen_enable_autorenew' );
770
771 if ( 1 < $total ) {
772 $hyperlink_url = add_query_arg(
773 array(
774 'utm_source' => 'pu',
775 'utm_campaign' => $utm_campaign,
776
777 ),
778 self::WOO_SUBSCRIPTION_PAGE_URL
779 );
780
781 $parsed_message = sprintf(
782 $messages['different_subscriptions'],
783 esc_attr( $total ),
784 esc_url( $hyperlink_url ),
785 esc_attr( $total ),
786 );
787
788 // All product ids.
789 $product_ids = array_map(
790 function ( $sub ) {
791 return $sub['product_id'];
792 },
793 $subs_to_show
794 );
795
796 return array(
797 'type' => 'different_subscriptions',
798 'parsed_message' => $parsed_message,
799 'product_id' => $product_ids,
800 );
801 }
802
803 $subscription = reset( $subs_to_show );
804 $product_id = $subscription['product_id'];
805 // check if $all_subs has multiple subs for this product.
806 $has_multiple_subs_for_product = 1 < count(
807 array_filter(
808 $all_subs,
809 function ( $sub ) use ( $product_id ) {
810 return $product_id === $sub['product_id'];
811 }
812 )
813 );
814
815 $message_key = $has_multiple_subs_for_product ? 'multiple_manage' : 'single_manage';
816
817 /**
818 * Even if there are multiple subscriptions for this product, if the store is covered by an active subscription,
819 * show the 'site covered' message instead of the 'manage' message.
820 */
821 if ( 'expired' === $type && $has_multiple_subs_for_product ) {
822 if ( self::has_active_usable_product_subscription( $product_id, $all_subs ) ) {
823 $message_key = 'multiple_manage_site_covered';
824 }
825 }
826
827 $renew_string = __( 'Renew', 'woocommerce' );
828 $subscribe_string = __( 'Subscribe', 'woocommerce' );
829 if ( isset( $subscription['product_regular_price'] ) ) {
830 /* translators: 1: Product price */
831 $renew_string = sprintf( __( 'Renew for %1$s', 'woocommerce' ), $subscription['product_regular_price'] );
832 }
833 $expiry_date = date_i18n( 'F jS', $subscription['expires'] );
834 $hyperlink_url = add_query_arg(
835 array(
836 'product_id' => $product_id,
837 'type' => $type,
838 'utm_source' => 'pu',
839 'utm_campaign' => $utm_campaign,
840
841 ),
842 self::WOO_SUBSCRIPTION_PAGE_URL
843 );
844
845 // Construct message based on template for multiple_manage or single_manage, parameter used:
846 // 1. Product name
847 // 2. Expiry date
848 // 3. URL to My Subscriptions page with extra params
849 // 4. Renew string.
850 if ( isset( $messages[ $message_key ] ) ) {
851 $parsed_message = sprintf(
852 $messages[ $message_key ],
853 esc_attr( $subscription['product_name'] ),
854 esc_attr( $expiry_date ),
855 esc_url( $hyperlink_url ),
856 // Show subscribe for missing subscriptions, renew otherwise.
857 'missing' === $type ? esc_attr( $subscribe_string ) : esc_attr( $renew_string ),
858 );
859
860 return array(
861 'type' => $message_key,
862 'parsed_message' => $parsed_message,
863 'product_id' => $product_id,
864 );
865 }
866
867 return array(
868 'type' => 'invalid',
869 'parsed_message' => '',
870 'product_id' => '',
871 );
872 }
873
874 /**
875 * Check whether the current store has an active usable subscription for a product.
876 *
877 * @param int $product_id Product id.
878 * @param array $subscriptions Subscription list data.
879 * @return bool
880 */
881 private static function has_active_usable_product_subscription( int $product_id, array $subscriptions ): bool {
882 $auth = \WC_Helper_Options::get( 'auth' );
883 $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
884
885 if ( 0 === $site_id ) {
886 return false;
887 }
888
889 foreach ( $subscriptions as $subscription ) {
890 if ( absint( $subscription['product_id'] ?? 0 ) !== $product_id ) {
891 continue;
892 }
893
894 $connections = isset( $subscription['connections'] ) && is_array( $subscription['connections'] ) ? $subscription['connections'] : array();
895 if ( ! in_array( $site_id, $connections, true ) ) {
896 continue;
897 }
898
899 if ( empty( $subscription['expired'] ) || ! empty( $subscription['lifetime'] ) ) {
900 return true;
901 }
902 }
903
904 return false;
905 }
906
907 /**
908 * Get formatted notice information for expiring subscription.
909 *
910 * @param boolean $allowed_link whether the notice description should include a link.
911 * @return array notice information.
912 */
913 public static function get_expiring_subscription_notice( $allowed_link = true ) {
914 if ( ! WC_Helper::is_site_connected() ) {
915 return array();
916 }
917
918 if ( self::$subscription_usage_notices_already_shown ) {
919 return array();
920 }
921
922 if ( ! self::should_show_notice( self::DISMISS_EXPIRING_SUBS_NOTICE ) ) {
923 return array();
924 }
925
926 $subscriptions = WC_Helper::get_subscription_list_data();
927 $expiring_subscriptions = array_filter(
928 $subscriptions,
929 function ( $sub ) {
930 return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
931 && ( $sub['active'] || empty( $sub['connections'] ) ) // Active on current site or not connected to any sites.
932 && $sub['expiring']
933 && ! $sub['autorenew'];
934 },
935 );
936
937 if ( ! $expiring_subscriptions ) {
938 return array();
939 }
940
941 $total_expiring_subscriptions = count( $expiring_subscriptions );
942
943 // Don't show missing notice if there are expiring subscriptions.
944 self::$subscription_usage_notices_already_shown = true;
945
946 // When payment method is missing on WooCommerce.com.
947 $helper_notices = WC_Helper::get_notices();
948 if ( ! empty( $helper_notices['missing_payment_method_notice'] ) ) {
949 return self::get_missing_payment_method_notice( $allowed_link, $total_expiring_subscriptions );
950 }
951
952 // Payment method is available but there are expiring subscriptions.
953 $notice_data = self::get_subscriptions_notice_data(
954 $subscriptions,
955 $expiring_subscriptions,
956 $total_expiring_subscriptions,
957 array(
958 /* translators: 1) product name 2) expiry date 3) URL to My Subscriptions page */
959 'single_manage' => __( 'Your subscription for <strong>%1$s</strong> expires on %2$s. <a href="%3$s">Enable auto-renewal</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
960 /* translators: 1) product name 2) expiry date 3) URL to My Subscriptions page */
961 'multiple_manage' => __( 'One of your subscriptions for <strong>%1$s</strong> expires on %2$s. <a href="%3$s">Enable auto-renewal</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
962 /* translators: 1) total expiring subscriptions 2) URL to My Subscriptions page */
963 'different_subscriptions' => __( 'You have <strong>%1$s Woo extension subscriptions</strong> expiring soon. <a href="%2$s">Enable auto-renewal</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
964 ),
965 'expiring',
966 );
967
968 $button_link = add_query_arg(
969 array(
970 'utm_source' => 'pu',
971 'utm_campaign' => 'pu_in_apps_screen_enable_autorenew',
972 ),
973 self::WOO_SUBSCRIPTION_PAGE_URL
974 );
975 if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
976 $button_link = add_query_arg(
977 array(
978 'product_id' => $notice_data['product_id'],
979 'type' => 'expiring',
980 ),
981 $button_link
982 );
983 }
984
985 return array(
986 'description' => $allowed_link ? $notice_data['parsed_message'] : preg_replace( '#<a.*?>(.*?)</a>#i', '\1', $notice_data['parsed_message'] ),
987 'button_text' => __( 'Enable auto-renewal', 'woocommerce' ),
988 'button_link' => $button_link,
989 );
990 }
991
992 /**
993 * Get formatted notice information for expired subscription.
994 *
995 * @param boolean $allowed_link whether the notice description should include a link.
996 * @return array notice information.
997 */
998 public static function get_expired_subscription_notice( $allowed_link = true ) {
999 if ( ! WC_Helper::is_site_connected() ) {
1000 return array();
1001 }
1002
1003 if ( ! self::should_show_notice( self::DISMISS_EXPIRED_SUBS_NOTICE ) ) {
1004 return array();
1005 }
1006
1007 $subscriptions = WC_Helper::get_subscription_list_data();
1008 $expired_subscriptions = array_filter(
1009 $subscriptions,
1010 function ( $sub ) {
1011 return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
1012 && ( $sub['active'] || empty( $sub['connections'] ) ) // Active on current site or not connected to any sites.
1013 && $sub['expired']
1014 && ! $sub['lifetime'];
1015 },
1016 );
1017
1018 if ( ! $expired_subscriptions ) {
1019 return array();
1020 }
1021
1022 $total_expired_subscriptions = count( $expired_subscriptions );
1023 self::$subscription_usage_notices_already_shown = true;
1024
1025 $notice_data = self::get_subscriptions_notice_data(
1026 $subscriptions,
1027 $expired_subscriptions,
1028 $total_expired_subscriptions,
1029 array(
1030 /* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
1031 'single_manage' => __( 'Your subscription for <strong>%1$s</strong> expired. <a href="%3$s">%4$s</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
1032 /* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
1033 'multiple_manage' => __( 'One of your subscriptions for <strong>%1$s</strong> has expired. <a href="%3$s">%4$s</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
1034 /* translators: 1) product name 3) URL to My Subscriptions page */
1035 'multiple_manage_site_covered' => __( 'One of your subscriptions for <strong>%1$s</strong> has expired. This store is still covered by another active subscription.', 'woocommerce' ),
1036 /* translators: 1) total expired subscriptions 2) URL to My Subscriptions page */
1037 'different_subscriptions' => __( 'You have <strong>%1$s Woo extension subscriptions</strong> that expired. <a href="%2$s">Renew</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
1038 ),
1039 'expired',
1040 );
1041
1042 $button_text = __( 'Renew', 'woocommerce' );
1043 $button_link = add_query_arg(
1044 array(
1045 'add-to-cart' => $notice_data['product_id'],
1046 'utm_source' => 'pu',
1047 'utm_campaign' => $allowed_link ? 'pu_settings_screen_renew' : 'pu_in_apps_screen_renew',
1048 ),
1049 self::WOO_CART_PAGE_URL
1050 );
1051
1052 if ( 'multiple_manage_site_covered' === $notice_data['type'] ) {
1053 $button_text = __( 'Review subscriptions', 'woocommerce' );
1054 $button_link = add_query_arg(
1055 array(
1056 'product_id' => $notice_data['product_id'],
1057 'type' => 'expired',
1058 'utm_source' => 'pu',
1059 'utm_campaign' => $allowed_link ? 'pu_settings_screen_review_subscriptions' : 'pu_in_apps_screen_review_subscriptions',
1060 ),
1061 self::WOO_SUBSCRIPTION_PAGE_URL
1062 );
1063 } elseif ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
1064 $button_link = add_query_arg(
1065 array(
1066 'add-to-cart' => $notice_data['product_id'],
1067 ),
1068 $button_link
1069 );
1070 }
1071
1072 return array(
1073 'description' => $allowed_link ? $notice_data['parsed_message'] : preg_replace( '#<a.*?>(.*?)</a>#i', '\1', $notice_data['parsed_message'] ),
1074 'button_text' => $button_text,
1075 'button_link' => $button_link,
1076 );
1077 }
1078
1079 /**
1080 * Get formatted notice information for missing subscription.
1081 *
1082 * @return array notice information.
1083 */
1084 public static function get_missing_subscription_notice() {
1085 if ( ! WC_Helper::is_site_connected() ) {
1086 return array();
1087 }
1088
1089 if ( self::$subscription_usage_notices_already_shown ) {
1090 return array();
1091 }
1092
1093 if ( ! self::should_show_notice( self::DISMISS_MISSING_SUBS_NOTICE ) ) {
1094 return array();
1095 }
1096
1097 $subscriptions = WC_Helper::get_subscription_list_data();
1098 $missing_subscriptions = array_filter(
1099 $subscriptions,
1100 function ( $sub ) {
1101 return ( ! empty( $sub['local']['installed'] ) && empty( $sub['product_key'] ) );
1102 },
1103 );
1104
1105 // Remove WUM from missing subscriptions list.
1106 $missing_subscriptions = array_filter(
1107 $missing_subscriptions,
1108 function ( $sub ) {
1109 return 'woo-update-manager' !== $sub['zip_slug'];
1110 }
1111 );
1112
1113 if ( ! $missing_subscriptions ) {
1114 return array();
1115 }
1116
1117 $total_missing_subscriptions = count( $missing_subscriptions );
1118
1119 $notice_data = self::get_subscriptions_notice_data(
1120 $subscriptions,
1121 $missing_subscriptions,
1122 $total_missing_subscriptions,
1123 array(
1124 /* translators: 1) product name */
1125 'single_manage' => __( 'You don\'t have a subscription for <strong>%1$s</strong>. Subscribe to receive updates and streamlined support.', 'woocommerce' ),
1126 /* translators: 1) total expired subscriptions */
1127 'different_subscriptions' => __( 'You don\'t have subscriptions for <strong>%1$s Woo extensions</strong>. Subscribe to receive updates and streamlined support.', 'woocommerce' ),
1128 ),
1129 'missing',
1130 );
1131
1132 $button_link = add_query_arg(
1133 array(
1134 'add-to-cart' => $notice_data['product_id'],
1135 'utm_source' => 'pu',
1136 'utm_campaign' => 'pu_in_apps_screen_purchase',
1137 ),
1138 self::WOO_CART_PAGE_URL
1139 );
1140
1141 if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
1142 $button_link = add_query_arg(
1143 array(
1144 'add-to-cart' => $notice_data['product_id'],
1145 ),
1146 $button_link
1147 );
1148 }
1149
1150 $button_text = __( 'Subscribe', 'woocommerce' );
1151
1152 return array(
1153 'description' => $notice_data['parsed_message'],
1154 'button_text' => $button_text,
1155 'button_link' => $button_link,
1156 );
1157 }
1158
1159 /**
1160 * Get notice information when WCCOM connection is disconnected.
1161 *
1162 * @return string disconnect notice.
1163 */
1164 public static function get_wccom_disconnected_notice() {
1165 if ( WC_Helper::is_site_connected() ) {
1166 return '';
1167 }
1168
1169 if ( ! self::should_show_notice( self::DISMISS_DISCONNECT_NOTICE, false ) ) {
1170 return '';
1171 }
1172
1173 $user_email = \WC_Helper_Options::get( 'last_disconnected_user_data' )['email'] ?? null;
1174 if ( empty( $user_email ) ) {
1175 return '';
1176 }
1177
1178 return sprintf(
1179 /* translators: 1: Disconnected user email */
1180 __( 'Successfully disconnected from <b>%1$s</b>.', 'woocommerce' ),
1181 $user_email
1182 );
1183 }
1184
1185 /**
1186 * Get the connected status notice message.
1187 *
1188 * @param string $user_email the user email.
1189 *
1190 * @return string the connected notice message.
1191 */
1192 public static function get_wccom_connected_notice( $user_email ) {
1193 if ( ! WC_Helper::is_site_connected() ) {
1194 return '';
1195 }
1196
1197 if ( ! self::should_show_notice( self::DISMISS_CONNECT_NOTICE, false ) ) {
1198 return '';
1199 }
1200
1201 if ( ! $user_email ) {
1202 return '';
1203 }
1204
1205 return sprintf(
1206 /* translators: 1: Disconnected user email */
1207 __( 'Successfully connected to <b>%s</b>.', 'woocommerce' ),
1208 $user_email
1209 );
1210 }
1211
1212 /**
1213 * Determine whether a specific notice should be shown to the current user.
1214 *
1215 * @param string $dismiss_notice_meta User meta that includes the timestamp when a store notice was dismissed.
1216 * @param bool $show_after_one_month Show the notices dismissed earlier than one month.
1217 * @return bool True if the notice should be shown, false otherwise.
1218 */
1219 public static function should_show_notice( $dismiss_notice_meta, $show_after_one_month = true ) {
1220 // Get the current user ID.
1221 $user_id = get_current_user_id();
1222
1223 // Get the timestamp when the notice was dismissed.
1224 $dismissed_timestamp = get_user_meta( $user_id, $dismiss_notice_meta, true );
1225
1226 if ( ! $show_after_one_month ) {
1227 return empty( $dismissed_timestamp );
1228 }
1229
1230 // If the notice was dismissed within the last month, do not show it.
1231 if ( ! empty( $dismissed_timestamp ) && ( time() - $dismissed_timestamp ) < 30 * DAY_IN_SECONDS ) {
1232 return false;
1233 }
1234
1235 // If the notice was dismissed more than a month ago, delete the meta value and show the notice.
1236 if ( ! empty( $dismissed_timestamp ) ) {
1237 delete_user_meta( $user_id, $dismiss_notice_meta );
1238 }
1239
1240 return true;
1241 }
1242
1243 /**
1244 * Get the notice data for missing payment method.
1245 *
1246 * @param bool $allowed_link whether should show link on the notice or not.
1247 * @param int $total_expiring_subscriptions total expiring subscriptions.
1248 *
1249 * @return array the notices data.
1250 */
1251 public static function get_missing_payment_method_notice( $allowed_link = true, $total_expiring_subscriptions = 1 ) {
1252 $add_payment_method_link = add_query_arg(
1253 array(
1254 'utm_source' => 'pu',
1255 'utm_campaign' => $allowed_link ? 'pu_settings_screen_add_payment_method' : 'pu_in_apps_screen_add_payment_method',
1256 ),
1257 self::WOO_ADD_PAYMENT_METHOD_URL
1258 );
1259 $description = $allowed_link
1260 ? sprintf(
1261 /* translators: %s: WooCommerce.com URL to add payment method */
1262 _n(
1263 'Your WooCommerce extension subscription is missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
1264 'Your WooCommerce extension subscriptions are missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
1265 $total_expiring_subscriptions,
1266 'woocommerce'
1267 ),
1268 $add_payment_method_link
1269 )
1270 : _n(
1271 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
1272 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
1273 $total_expiring_subscriptions,
1274 'woocommerce'
1275 );
1276
1277 return array(
1278 'description' => $description,
1279 'button_text' => __( 'Add payment method', 'woocommerce' ),
1280 'button_link' => $add_payment_method_link,
1281 );
1282 }
1283 }
1284