: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
* Represents the addon manager.
class WPSEO_Addon_Manager {
* Holds the name of the transient.
public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
* Holds the name of the transient.
public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick';
* Holds the slug for YoastSEO free.
public const FREE_SLUG = 'yoast-seo-wordpress';
* Holds the slug for YoastSEO Premium.
public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
* Holds the slug for Yoast News.
public const NEWS_SLUG = 'yoast-seo-news';
* Holds the slug for Video.
public const VIDEO_SLUG = 'yoast-seo-video';
* Holds the slug for WooCommerce.
public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
* Holds the slug for Local.
public const LOCAL_SLUG = 'yoast-seo-local';
* The expected addon data.
protected static $addons = [
'wp-seo-premium.php' => self::PREMIUM_SLUG,
'wpseo-news.php' => self::NEWS_SLUG,
'video-seo.php' => self::VIDEO_SLUG,
'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG,
'local-seo.php' => self::LOCAL_SLUG,
* The addon data for the shortlinks.
private $addon_details = [
'name' => 'Yoast SEO Premium',
'short_link_activation' => 'https://yoa.st/13j',
'short_link_renewal' => 'https://yoa.st/4ey',
'name' => 'Yoast News SEO',
'short_link_activation' => 'https://yoa.st/4xq',
'short_link_renewal' => 'https://yoa.st/4xv',
self::WOOCOMMERCE_SLUG => [
'name' => 'Yoast WooCommerce SEO',
'short_link_activation' => 'https://yoa.st/4xs',
'short_link_renewal' => 'https://yoa.st/4xx',
'name' => 'Yoast Video SEO',
'short_link_activation' => 'https://yoa.st/4xr',
'short_link_renewal' => 'https://yoa.st/4xw',
'name' => 'Yoast Local SEO',
'short_link_activation' => 'https://yoa.st/4xp',
'short_link_renewal' => 'https://yoa.st/4xu',
* Holds the site information data.
private $site_information;
public function register_hooks() {
add_action( 'admin_init', [ $this, 'validate_addons' ], 15 );
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 );
add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 );
* Registers "expired subscription" warnings to the update messages of our addons.
public function register_expired_messages() {
foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) {
add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 );
* Gets the subscriptions for current site.
* @return stdClass The subscriptions.
public function get_subscriptions() {
return $this->get_site_information()->subscriptions;
* Provides a list of addon filenames.
* @return string[] List of addon filenames with their slugs.
public function get_addon_filenames() {
* @param string $plugin_slug The plugin slug to search.
* @return bool|string Plugin file when installed, False when plugin isn't installed.
public function get_plugin_file( $plugin_slug ) {
$plugins = $this->get_plugins();
$plugin_files = array_keys( $plugins );
$target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true );
if ( ! $target_plugin_file ) {
foreach ( $plugin_files as $plugin_file ) {
if ( strpos( $plugin_file, $target_plugin_file ) !== false ) {
* Retrieves the subscription for the given slug.
* @param string $slug The plugin slug to retrieve.
* @return stdClass|false Subscription data when found, false when not found.
public function get_subscription( $slug ) {
foreach ( $this->get_subscriptions() as $subscription ) {
if ( $subscription->product->slug === $slug ) {
* Retrieves a list of (subscription) slugs by the active addons.
* @return array The slugs.
public function get_subscriptions_for_active_addons() {
$active_addons = array_keys( $this->get_active_addons() );
$subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
foreach ( $subscription_slugs as $subscription_slug ) {
$subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug );
* Retrieves a list of versions for each addon.
* @return array The addon versions.
public function get_installed_addons_versions() {
foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
$addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version'];
* Retrieves the plugin information from the subscriptions.
* @param stdClass|false $data The result object. Default false.
* @param string $action The type of information being requested from the Plugin Installation API.
* @param stdClass $args Plugin API arguments.
* @return object Extended plugin data.
public function get_plugin_information( $data, $action, $args ) {
if ( $action !== 'plugin_information' ) {
if ( ! isset( $args->slug ) ) {
$subscription = $this->get_subscription( $args->slug );
$data = $this->convert_subscription_to_plugin( $subscription, null, true );
if ( $this->has_subscription_expired( $subscription ) ) {
unset( $data->package, $data->download_link );
* Retrieves information from MyYoast about which addons are connected to the current site.
* @return stdClass The list of addons activated for this site.
public function get_myyoast_site_information() {
if ( $this->site_information === null ) {
$this->site_information = $this->get_site_information_transient();
if ( $this->site_information ) {
return $this->site_information;
$this->site_information = $this->request_current_sites();
if ( $this->site_information ) {
$this->site_information = $this->map_site_information( $this->site_information );
$this->set_site_information_transient( $this->site_information );
return $this->site_information;
return $this->get_site_information_default();
* Checks if the subscription for the given slug is valid.
* @param string $slug The plugin slug to retrieve.
* @return bool True when the subscription is valid.
public function has_valid_subscription( $slug ) {
$subscription = $this->get_subscription( $slug );
// An non-existing subscription is never valid.
return ! $this->has_subscription_expired( $subscription );
* Checks if there are addon updates.
* @param stdClass|mixed $data The current data for update_plugins.
* @return stdClass Extended data for update_plugins.
public function check_for_updates( $data ) {
// We have to figure out if we're safe to upgrade the add-ons, based on what the latest Yoast Free requirements for the WP version is.
$yoast_free_data = $this->extract_yoast_data( $data );
foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) {
$subscription_slug = $this->get_slug_by_plugin_file( $plugin_file );
$subscription = $this->get_subscription( $subscription_slug );
$plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file );
// Let's assume for now that it will get added in the 'no_update' key that we'll return to the WP API.
// If the add-on's version is the latest, we have to do no further checks.
if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) {
// If we haven't retrieved the Yoast Free requirements for the WP version yet, do nothing. The next run will probably get us that information.
if ( is_null( $plugin_data->requires ) ) {
if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
// The add-on has an available update *and* the Yoast Free requirements for the WP version are also met, so go ahead and show the upgrade info to the user.
$data->response[ $plugin_file ] = $plugin_data;
if ( $this->has_subscription_expired( $subscription ) ) {
unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link );
// Still convert subscription when no updates is available.
$data->no_update[ $plugin_file ] = $plugin_data;
if ( $this->has_subscription_expired( $subscription ) ) {
unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link );
* Extracts Yoast SEO Free's data from the wp.org API response.
* @param object $data The wp.org API response.
* @return object Yoast Free's data from wp.org.
protected function extract_yoast_data( $data ) {
if ( isset( $data->response[ WPSEO_BASENAME ] ) ) {
return $data->response[ WPSEO_BASENAME ];
if ( isset( $data->no_update[ WPSEO_BASENAME ] ) ) {
return $data->no_update[ WPSEO_BASENAME ];
* If the plugin is lacking an active subscription, throw a warning.
* @param array $plugin_data The data for the plugin in this row.
public function expired_subscription_warning( $plugin_data ) {
$subscription = $this->get_subscription( $plugin_data['slug'] );
if ( $subscription && $this->has_subscription_expired( $subscription ) ) {
$addon_link = ( isset( $this->addon_details[ $plugin_data['slug'] ] ) ) ? $this->addon_details[ $plugin_data['slug'] ]['short_link_renewal'] : $this->addon_details[ self::PREMIUM_SLUG ]['short_link_renewal'];
if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2023-promotion' ) ) {
/* translators: %1$s is a <br> tag. */
esc_html__( '%1$s Now with 30%% Black Friday Discount!', 'wordpress-seo' ),
echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> '
/* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
esc_html__( '%1$s can\'t be updated because your product subscription is expired. %2$sRenew your product subscription%3$s to get updates again and use all the features of %1$s.', 'wordpress-seo' ),
esc_html( $plugin_data['name'] ),
'<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
* Checks if there are any installed addons.
* @return bool True when there are installed Yoast addons.
public function has_installed_addons() {
$installed_addons = $this->get_installed_addons();
return ! empty( $installed_addons );
* Checks if the plugin is installed and activated in WordPress.
* @param string $slug The class' slug.
* @return bool True when installed and activated.
public function is_installed( $slug ) {
static::PREMIUM_SLUG => 'WPSEO_Premium',
static::NEWS_SLUG => 'WPSEO_News',
static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
static::VIDEO_SLUG => 'WPSEO_Video_Sitemap',
static::LOCAL_SLUG => 'WPSEO_Local_Core',
if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
return class_exists( $slug_to_class_map[ $slug ] );
* Validates the addons and show a notice for the ones that are invalid.
public function validate_addons() {
$notification_center = Yoast_Notification_Center::get();
if ( $notification_center === null ) {
foreach ( $this->addon_details as $slug => $addon_info ) {
$notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
// Add a notification when the installed plugin isn't activated in My Yoast.
if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
$notification_center->add_notification( $notification );
$notification_center->remove_notification( $notification );
* Removes the site information transients.
public function remove_site_information_transients() {
delete_transient( self::SITE_INFORMATION_TRANSIENT );
delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );