Files
2026-04-28 15:13:50 +02:00

845 lines
22 KiB
PHP

<?php
namespace AIOSEO\Plugin\Pro\Admin;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The License class to validate/activate/deactivate license keys.
*
* @since 4.2.5
*/
class NetworkLicense extends License {
/**
* The Action Scheduler action name for the recurring daily network license check.
*
* @since 4.9.5.2
*
* @var string
*/
public $licenseCheckAction = 'aioseo_network_license_check';
/**
* The Action Scheduler action name for the expired network license retry check.
*
* @since 4.9.5.2
*
* @var string
*/
public $licenseCheckExpiredAction = 'aioseo_network_license_check_expired';
/**
* The Action Scheduler action name for resetting AI access tokens across subsites.
*
* @since 4.9.5.2
*
* @var string
*/
private $resetSubsiteTokensAction = 'aioseo_ai_reset_subsite_tokens';
/**
* Class constructor.
*
* @since 4.2.5
*/
public function __construct() {
$this->options = aioseo()->networkOptions;
$this->internalOptions = aioseo()->internalNetworkOptions;
$this->sensitiveOptions = aioseo()->networkSensitiveOptions;
$this->sensitiveKeyName = 'networkLicenseKey';
add_action( 'admin_init', [ $this, 'maybeActivateFromConstant' ], 2 );
add_action( 'admin_init', [ $this, 'scheduleLicenseCheck' ], 3 );
add_action( $this->licenseCheckAction, [ $this, 'checkLicense' ] );
add_action( $this->licenseCheckExpiredAction, [ $this, 'checkLicense' ] );
add_action( $this->resetSubsiteTokensAction, [ $this, 'resetSubsiteTokens' ] );
include_once ABSPATH . 'wp-admin/includes/plugin.php';
if (
is_network_admin() &&
! is_plugin_active_for_network( plugin_basename( AIOSEO_FILE ) )
) {
return;
}
// phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized, HM.Security.NonceVerification.Recommended
if ( ! isset( $_GET['page'] ) || 'aioseo-settings' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {
add_action( 'network_admin_notices', [ $this, 'notices' ] );
}
// phpcs:enable
add_action( 'after_plugin_row_' . AIOSEO_PLUGIN_BASENAME, [ $this, 'pluginRowNotice' ] );
add_action( 'in_plugin_update_message-' . AIOSEO_PLUGIN_BASENAME, [ $this, 'updateRowNotice' ] );
}
/**
* Activates the network license from the AIOSEO_LICENSE_KEY constant if defined and different from the stored key.
*
* Deferred to admin_init so the full aioseo() container (actionScheduler, etc.) is ready.
*
* @since 4.9.6.2
*
* @return void
*/
public function maybeActivateFromConstant() {
if ( ! defined( 'AIOSEO_LICENSE_KEY' ) || empty( AIOSEO_LICENSE_KEY ) ) {
return;
}
if ( AIOSEO_LICENSE_KEY === $this->sensitiveOptions->get( $this->sensitiveKeyName ) ) {
return;
}
$this->sensitiveOptions->set( $this->sensitiveKeyName, AIOSEO_LICENSE_KEY );
aioseo()->actionScheduler->unschedule( $this->licenseCheckAction );
$this->activate();
}
/**
* Validate the license keys for a multisite setup.
*
* @since 4.2.5
*
* @param array $domains Domains for activation and deactivation.
* @return boolean Whether or not it was activated.
*/
public function multisite( $domains ) {
aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level',
'addons',
'counts',
'upgradeUrl'
]
);
$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
if ( empty( $licenseKey ) ) {
aioseo()->helpers->restoreCurrentBlog();
return false;
}
$site = aioseo()->helpers->getSite();
$domains = ! empty( $domains )
? $domains
: [
[
'domain' => $site->domain,
'path' => $site->path
]
];
$response = $this->sendLicenseRequest( 'multisite', $licenseKey, $domains );
if ( empty( $response ) ) {
// Something bad happened, error unknown.
$this->internalOptions->internal->license->connectionError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( ! empty( $response->error ) ) {
if ( 'missing-key-or-domain' === $response->error ) {
$this->internalOptions->internal->license->requestError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'missing-license' === $response->error ) {
$this->internalOptions->internal->license->invalid = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'disabled' === $response->error ) {
$this->internalOptions->internal->license->disabled = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'activations' === $response->error ) {
$this->internalOptions->internal->license->activationsError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'expired' === $response->error ) {
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->expired = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
}
// Something bad happened, error unknown.
if ( empty( $response->success ) || empty( $response->level ) ) {
aioseo()->helpers->restoreCurrentBlog();
return false;
}
$this->internalOptions->internal->license->level = $response->level;
$this->internalOptions->internal->license->addons = wp_json_encode( $response->addons );
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->features = wp_json_encode( $response->features );
// Store activation counts if provided.
if ( ! empty( $response->counts ) ) {
$this->internalOptions->internal->license->counts = wp_json_encode( $response->counts );
}
// Store upgrade URL if provided.
if ( ! empty( $response->upgradeUrl ) ) {
$this->internalOptions->internal->license->upgradeUrl = $response->upgradeUrl;
}
aioseo()->helpers->restoreCurrentBlog();
if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
$activatedIds = ! empty( $domains['activate'] ) ? array_column( $domains['activate'], 'blog_id' ) : [];
$deactivatedIds = ! empty( $domains['deactivate'] ) ? array_column( $domains['deactivate'], 'blog_id' ) : [];
$blogIds = array_values( array_filter( array_merge( $activatedIds, $deactivatedIds ) ) );
$this->scheduleTokenReset( $blogIds );
}
$this->clearSitesActiveCache( $domains );
return true;
}
/**
* Validate the license key.
*
* @since 4.2.5
*
* @param array $newDomains New domains to activate.
* @return boolean Whether or not it was activated.
*/
public function activate( $newDomains = [] ) {
aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level',
'addons',
'features',
'counts',
'upgradeUrl'
]
);
$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
if ( empty( $licenseKey ) ) {
aioseo()->helpers->restoreCurrentBlog();
return false;
}
$site = aioseo()->helpers->getSite();
$domains = ! empty( $newDomains )
? $newDomains
: [
[
'domain' => $site->domain,
'path' => $site->path
]
];
$response = $this->sendLicenseRequest( 'activate', $licenseKey, $domains );
if ( empty( $response ) ) {
// Something bad happened, error unknown.
$this->internalOptions->internal->license->connectionError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( ! empty( $response->error ) ) {
if ( 'missing-key-or-domain' === $response->error ) {
$this->internalOptions->internal->license->requestError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'missing-license' === $response->error ) {
$this->internalOptions->internal->license->invalid = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'disabled' === $response->error ) {
$this->internalOptions->internal->license->disabled = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'domain-disabled' === $response->error ) {
$this->internalOptions->internal->license->domainDisabled = true;
return false;
}
if ( 'activations' === $response->error ) {
$this->internalOptions->internal->license->activationsError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'expired' === $response->error ) {
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->expired = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
}
// Something bad happened, error unknown.
if ( empty( $response->success ) || empty( $response->level ) ) {
aioseo()->helpers->restoreCurrentBlog();
return false;
}
$this->internalOptions->internal->license->level = $response->level;
$this->internalOptions->internal->license->addons = wp_json_encode( $response->addons );
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->features = wp_json_encode( $response->features );
// Store activation counts if provided.
if ( ! empty( $response->counts ) ) {
$this->internalOptions->internal->license->counts = wp_json_encode( $response->counts );
}
// Store upgrade URL if provided.
if ( ! empty( $response->upgradeUrl ) ) {
$this->internalOptions->internal->license->upgradeUrl = $response->upgradeUrl;
}
aioseo()->helpers->restoreCurrentBlog();
if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
$this->scheduleTokenReset();
}
$this->clearSitesActiveCache( $newDomains );
return true;
}
/**
* Deactivate the license key.
*
* @since 4.2.5
*
* @param array $domains New domains to activate.
* @return boolean Whether it was deactivated.
*/
public function deactivate( $domains = [] ) {
aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
if ( empty( $licenseKey ) ) {
aioseo()->helpers->restoreCurrentBlog();
return false;
}
$site = aioseo()->helpers->getSite();
$domainsToDeactivate = ! empty( $domains )
? $domains
: [
[
'domain' => $site->domain,
'path' => $site->path
]
];
$response = $this->sendLicenseRequest( 'deactivate', $licenseKey, $domainsToDeactivate );
if ( empty( $response ) ) {
// Something bad happened, error unknown.
$this->internalOptions->internal->license->connectionError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( ! empty( $response->error ) ) {
if ( 'missing-key-or-domain' === $response->error || 'not-activated' === $response->error ) {
$this->internalOptions->internal->license->requestError = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'missing-license' === $response->error ) {
$this->internalOptions->internal->license->invalid = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'disabled' === $response->error ) {
$this->internalOptions->internal->license->disabled = true;
aioseo()->helpers->restoreCurrentBlog();
return false;
}
if ( 'domain-disabled' === $response->error ) {
$this->internalOptions->internal->license->domainDisabled = true;
return false;
}
}
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level',
'addons'
]
);
$this->internalOptions->internal->license->level = $response->level;
$this->internalOptions->internal->license->addons = wp_json_encode( $response->addons );
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->features = wp_json_encode( $response->features );
aioseo()->seoChecklist->uncompleteCheck( 'licenseActivated' );
aioseo()->helpers->restoreCurrentBlog();
if ( aioseo()->internalOptions->internal->has( 'ai' ) ) {
$this->scheduleTokenReset();
}
$this->clearSitesActiveCache( $domains );
return true;
}
/**
* Schedules a one-time action to clear AI access tokens for subsites.
*
* @since 4.9.5.2
*
* @param array $blogIds Specific blog IDs to reset. Empty clears all subsites.
* @return void
*/
private function scheduleTokenReset( $blogIds = [] ) {
aioseo()->actionScheduler->scheduleSingle( $this->resetSubsiteTokensAction, 0, [ $blogIds ] );
}
/**
* Resets all AI settings (access token, credits, etc.) for the given subsites (or all subsites if none specified).
* Hooked into `aioseo_ai_reset_subsite_tokens` action hook.
*
* Each affected subsite will fetch a fresh token and credits on its next admin load via
* the scheduled `aioseo_ai_get_access_token` action.
*
* @since 4.9.5.2
*
* @param array $blogIds Specific blog IDs to reset. Empty clears all subsites.
* @return void
*/
public function resetSubsiteTokens( $blogIds = [] ) {
if ( ! is_multisite() ) {
return;
}
$sites = ! empty( $blogIds )
? $blogIds
: get_sites( [
'fields' => 'ids',
'number' => 0
] );
$optionName = 'aioseo_options_internal';
foreach ( $sites as $blogId ) {
$json = get_blog_option( (int) $blogId, $optionName );
if ( ! $json ) {
continue;
}
$options = json_decode( $json, true );
if ( empty( $options['internal']['ai'] ) ) {
continue;
}
$options['internal']['ai'] = [];
update_blog_option( (int) $blogId, $optionName, wp_json_encode( $options ) );
}
$sensitiveOptionName = 'aioseo_sensitive_options';
foreach ( $sites as $blogId ) {
$json = get_blog_option( (int) $blogId, $sensitiveOptionName );
if ( ! $json ) {
continue;
}
$options = json_decode( $json, true );
if ( empty( $options['aiAccessToken'] ) ) {
continue;
}
$options['aiAccessToken'] = '';
update_blog_option( (int) $blogId, $sensitiveOptionName, wp_json_encode( $options ) );
}
}
/**
* Validates the stored license within the main network site context.
*
* Overrides the parent method to ensure that `getSite()` returns the main
* network site's domain, not a random subsite that Action Scheduler may
* execute from.
*
* @since 4.9.5.2
*
* @return void
*/
public function checkLicense() {
aioseo()->helpers->switchToBlog( aioseo()->helpers->getNetworkId() );
parent::checkLicense();
aioseo()->helpers->restoreCurrentBlog();
}
/**
* Checks to see if the current license is expired.
*
* @since 4.2.5
*
* @return bool True if expired, false if not.
*/
public function isExpired() {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return false;
}
if ( $this->internalOptions->internal->license->expired ) {
return true;
}
$expires = $this->internalOptions->internal->license->expires;
return 0 !== $expires && $expires < time();
}
/**
* Checks to see if the current license is disabled.
*
* @since 4.2.5
*
* @return bool True if disabled, false if not.
*/
public function isDisabled() {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return false;
}
return $this->internalOptions->internal->license->disabled;
}
/**
* Checks to see if the current license is invalid.
*
* @since 4.2.5
*
* @return bool True if invalid, false if not.
*/
public function isInvalid() {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return false;
}
return $this->internalOptions->internal->license->invalid;
}
/**
* Checks to see if the current license is disabled.
*
* @since 4.2.5
*
* @param \WP_Site $site The site to check if the the license is active on.
* @return bool True if disabled, false if not.
*/
public function isActive( $site = null ) {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return false;
}
if ( ! $this->isSiteActive( $site ) ) {
return false;
}
return ! $this->isExpired() && ! $this->isDisabled() && ! $this->isInvalid();
}
/**
* Get the license level for the activated license.
*
* @since 4.2.5
*
* @param \WP_Site $site The site to check if the the license is active on.
* @return string The license level.
*/
public function getLicenseLevel( $site = null ) {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return 'Unknown';
}
if ( ! $this->isSiteActive( $site ) ) {
return 'Unknown';
}
return $this->internalOptions->internal->license->level;
}
/**
* Checks if the current site is licensed at the network level.
*
* @since 4.2.5
*
* @return bool True if licensed at the network level.
*/
public function isNetworkLicensed() {
// If we are already locally activated, then no it's not network licensed.
if ( aioseo()->license->isActive() ) {
return false;
}
if ( $this->isActive() ) {
return true;
}
return false;
}
/**
* Checks if a given site (or the current one) is active.
*
* @since 4.2.5
* @version 4.4.0
*
* @param \WP_Site $site The site to check.
* @return bool True if active, false if not.
*/
public function isSiteActive( $site = null ) {
if ( ! aioseo()->networkSensitiveOptions->hasValue( 'networkLicenseKey' ) ) {
return false;
}
if ( empty( $site ) ) {
$site = \WP_Site::get_instance( get_current_blog_id() );
}
$urlHash = sha1( $site->domain . $site->path );
$cacheKey = "site_active_{$urlHash}";
$isActive = aioseo()->core->cache->get( $cacheKey );
if ( null !== $isActive ) {
return $isActive;
}
$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
$response = $this->sendLicenseRequest( 'activated', $licenseKey, [ $site ] );
if ( ! empty( $response->error ) || empty( $response->all_activations_and_paths ) ) {
aioseo()->core->cache->update( $cacheKey, false, DAY_IN_SECONDS );
return false;
}
$active = false;
foreach ( $response->all_activations_and_paths as $activeSite ) {
if ( $site->domain === $activeSite->domain && $site->path === $activeSite->path ) {
$active = true;
break;
}
if ( ! empty( $site->aliases ) ) {
foreach ( $site->aliases as $alias ) {
if ( $activeSite->domain === $alias['domain'] ) {
$active = true;
break;
}
}
}
}
aioseo()->core->cache->update( $cacheKey, $active, DAY_IN_SECONDS );
return $active;
}
/**
* Checks if the given sites are activated.
*
* @since 4.4.0
*
* @param array $domains The domains to check.
* @return array The domains with the active status.
*/
public function areSitesActive( $domains ) {
// Force domains to be objects.
$domains = json_decode( wp_json_encode( $domains ) );
$fullDomains = array_map( function( $domain ) {
return $domain->domain . $domain->path;
}, $domains );
$domainsHash = sha1( implode( ',', $fullDomains ) );
$cacheKey = "sites_active_{$domainsHash}";
$areActive = aioseo()->core->cache->get( $cacheKey );
if ( null !== $areActive ) {
return $areActive;
}
$licenseKey = aioseo()->networkSensitiveOptions->get( 'networkLicenseKey' );
$response = $this->sendLicenseRequest( 'activated', $licenseKey, $domains );
if ( ! empty( $response->error ) || empty( $response->all_activations_and_paths ) ) {
aioseo()->core->cache->update( $cacheKey, [], HOUR_IN_SECONDS );
return [];
}
$activeSites = [];
foreach ( $domains as $domain ) {
foreach ( $response->all_activations_and_paths as $activeSite ) {
if ( $domain->domain === $activeSite->domain && $domain->path === $activeSite->path ) {
$activeSites[] = $activeSite;
break;
}
if ( ! empty( $domain->aliases ) ) {
foreach ( $domain->aliases as $alias ) {
if ( $activeSite->domain === $alias['domain'] ) {
$activeSites[] = $activeSite;
break;
}
}
}
}
}
aioseo()->core->cache->update( $cacheKey, $activeSites, HOUR_IN_SECONDS );
return $activeSites;
}
/**
* Adds a notice to the update row for unlicensed users.
*
* @since 4.2.5
*
* @return void
*/
public function updateRowNotice() {
if ( $this->isActive() ) {
return;
}
$this->outputUpdateRowNotice();
}
/**
* Add row to Plugins page with licensing information, if license key is invalid or not found.
*
* @since 4.2.5
*
* @return void
*/
public function pluginRowNotice() {
// Don't output the notice on subsites since to prevent duplicate notices.
if ( $this->isActive() || ! is_main_site() ) {
return;
}
$this->outputPluginRowNotice();
}
/**
* Clears the active site(s) cache.
*
* @since 4.4.0
*
* @return void
*/
private function clearSitesActiveCache( $domains = [] ) {
$cacheTableName = aioseo()->core->db->prefix . 'aioseo_cache';
aioseo()->core->db->execute( "DELETE FROM $cacheTableName WHERE `key` LIKE 'sites_active_%'" );
$allDomains = [];
if ( isset( $domains['activate'] ) ) {
$allDomains = array_merge( $allDomains, $domains['activate'] );
}
if ( isset( $domains['deactivate'] ) ) {
$allDomains = array_merge( $allDomains, $domains['deactivate'] );
}
foreach ( $allDomains as $domain ) {
aioseo()->helpers->switchToBlog( $domain['blog_id'] );
$cacheTableName = aioseo()->core->db->prefix . 'aioseo_cache';
aioseo()->core->db->execute( "DELETE FROM $cacheTableName WHERE `key` LIKE 'site_active_%'" );
aioseo()->helpers->restoreCurrentBlog();
}
}
}