• File: ExtensionsWithoutSubscription.php
  • Full Path: /home/slvgwrnz/public_html/wp-content/plugins/woocommerce/src/Internal/Admin/Notes/ExtensionsWithoutSubscription.php
  • Date Modified: 10/26/2026 10:38 AM
  • File size: 6.14 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php
declare( strict_types = 1 );

namespace Automattic\WooCommerce\Internal\Admin\Notes;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\PageController;

/**
 * Inbox note for active WooCommerce.com extensions that have no subscription on this store.
 */
class ExtensionsWithoutSubscription {
	use NoteTraits;

	/**
	 * Name of the note for use in the database.
	 */
	const NOTE_NAME = 'wc-admin-extensions-without-subscription';

	/**
	 * Option holding when the note last fetched the subscription list from WooCommerce.com.
	 */
	const LAST_FETCH_OPTION_KEY = 'woocommerce_admin-extensions-without-subscription-last-fetch';

	/**
	 * Option holding the plugin files the note was last refreshed for, so unchanged page loads skip the notes table.
	 */
	const LAST_PLUGINS_OPTION_KEY = 'woocommerce_admin-extensions-without-subscription-last-plugins';

	/**
	 * Hook the note refresh.
	 *
	 * @since 11.3.0
	 */
	public function __construct() {
		add_action( 'admin_head', array( $this, 'refresh_note' ) );
	}

	/**
	 * Add, update or remove the note on WooCommerce admin pages, so it follows the installed extensions.
	 *
	 * Runs on every WooCommerce admin page, so it only touches the notes table when the set of
	 * extensions without a subscription differs from the last refresh.
	 *
	 * @since 11.3.0
	 */
	public function refresh_note(): void {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		// Try the cached subscription list first, so a store with nothing to check never reads the fetch budget.
		$plugins = self::get_plugins( false );
		if ( null === $plugins && self::may_fetch() ) {
			$plugins = self::get_plugins( true );
		}

		// Leave the note as it is while the subscription list can't be trusted.
		if ( null === $plugins ) {
			return;
		}

		$plugin_files = array_keys( $plugins );
		sort( $plugin_files );
		if ( get_option( self::LAST_PLUGINS_OPTION_KEY ) === $plugin_files ) {
			return;
		}

		if ( empty( $plugins ) ) {
			self::delete_note_unless_dismissed_or_actioned();
		} elseif ( self::note_exists() ) {
			self::possibly_update_note();
		} else {
			self::possibly_add_note();
		}

		update_option( self::LAST_PLUGINS_OPTION_KEY, $plugin_files, true );
	}

	/**
	 * Get the note.
	 *
	 * @since 11.3.0
	 *
	 * @return Note|null
	 */
	public static function get_note() {
		$plugins = self::get_plugins();
		if ( empty( $plugins ) ) {
			return null;
		}

		if ( 1 === count( $plugins ) ) {
			$title = sprintf(
				/* translators: %s: extension name */
				__( '%s doesn\'t have an active subscription', 'woocommerce' ),
				wp_strip_all_tags( current( $plugins )['Name'] )
			);
			$content = __( 'Without one, it won\'t receive security updates, product improvements, or support. Subscribe on WooCommerce.com to keep it protected.', 'woocommerce' );
		} else {
			$title   = __( 'Some of your extensions don\'t have an active subscription', 'woocommerce' );
			$content = __( 'Without one, they won\'t receive security updates, product improvements, or support. Subscribe on WooCommerce.com to keep them protected.', 'woocommerce' );
		}

		$note = new Note();
		$note->set_title( $title );
		$note->set_content( $content );
		$note->set_content_data( (object) array( 'plugins' => array_keys( $plugins ) ) );
		$note->set_type( Note::E_WC_ADMIN_NOTE_WARNING );
		$note->set_name( self::NOTE_NAME );
		$note->set_source( 'woocommerce-admin' );
		$note->add_action(
			'subscribe',
			__( 'Subscribe', 'woocommerce' ),
			add_query_arg(
				array(
					'page'         => 'wc-admin',
					'tab'          => 'my-subscriptions',
					'path'         => rawurlencode( '/extensions' ),
					'utm_source'   => 'inbox_notification',
					'utm_campaign' => 'pu_inbox_purchase',
				),
				admin_url( 'admin.php' )
			)
		);

		return $note;
	}

	/**
	 * Delete the note now that nothing is missing a subscription, unless the merchant already dealt with it.
	 *
	 * A dismissed or actioned note is hidden anyway, and keeping it means it stays that way if the
	 * extensions come back, for example after deactivating every plugin to find a conflict.
	 *
	 * @return void
	 */
	private static function delete_note_unless_dismissed_or_actioned(): void {
		$note = Notes::get_note_by_name( self::NOTE_NAME );
		if ( ! $note instanceof Note ) {
			return;
		}

		if ( $note->get_is_deleted() || Note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
			return;
		}

		self::possibly_delete_note();
	}

	/**
	 * Whether the note should exist.
	 *
	 * The note follows the installed extensions through refresh_note(), so it's always
	 * applicable once added.
	 *
	 * @return bool
	 */
	public static function is_applicable(): bool {
		return true;
	}

	/**
	 * Whether this page load may fetch the subscription list from WooCommerce.com.
	 *
	 * The fetch blocks the page, so only a connected store without a cached list fetches: once a day,
	 * or right after it connects.
	 *
	 * @return bool
	 */
	private static function may_fetch(): bool {
		if ( ! class_exists( 'WC_Helper' ) ) {
			return false;
		}

		// Nothing to fetch: a store that isn't connected has no list, and a cached one is already in use.
		if ( ! \WC_Helper::is_site_connected() || \WC_Helper::has_cached_subscriptions() ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read only, the same signal WooSubscriptionsNotes uses.
		if ( isset( $_GET['wc-helper-status'] ) ) {
			return true;
		}

		$now = time();
		if ( (int) get_option( self::LAST_FETCH_OPTION_KEY, 0 ) + DAY_IN_SECONDS > $now ) {
			return false;
		}

		update_option( self::LAST_FETCH_OPTION_KEY, $now, false );

		return true;
	}

	/**
	 * Active WooCommerce.com extensions without a subscription, or null when that can't be known.
	 *
	 * @param bool $fetch Whether the subscription list may be fetched from WooCommerce.com when it isn't cached.
	 * @return array|null
	 */
	private static function get_plugins( bool $fetch = true ): ?array {
		if ( ! class_exists( 'WC_Helper_Updater' ) ) {
			return null;
		}

		return \WC_Helper_Updater::get_plugins_without_subscription( $fetch );
	}
}