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(); } } }