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

518 lines
12 KiB
PHP

<?php
namespace AIOSEO\BrokenLinkChecker\Admin;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\BrokenLinkChecker\Models;
/**
* Handles license update/removal and related notices.
*
* @since 1.0.0
*/
class License {
/**
* The base URL for the licensing API.
*
* @since 1.0.0
*
* @var string
*/
private $baseUrl = 'https://blc-licensing.aioseo.com/v1/';
/**
* The action name for the periodic license check.
*
* @since 1.2.9
*
* @var string
*/
public $actionName = 'aioseo_blc_license_check';
/**
* Options class instance.
*
* @since 1.0.0
*
* @var \AIOSEO\BrokenLinkChecker\Options\Options
*/
protected $options = null;
/**
* InternalOptions class instance.
*
* @since 1.0.0
*t
* @var \AIOSEO\BrokenLinkChecker\Options\InternalOptions
*/
protected $internalOptions = null;
/**
* Class constructor.
*
* @since 1.0.0
*/
public function __construct() {
$this->internalOptions = aioseoBrokenLinkChecker()->internalOptions;
add_action( 'init', [ $this, 'scheduleLicenseCheck' ], 3 );
add_action( $this->actionName, [ $this, 'checkLicense' ] );
}
/**
* Schedules the license check as a recurring Action Scheduler action.
*
* @since 1.2.9
*
* @return void
*/
public function scheduleLicenseCheck() {
if ( ! $this->internalOptions->internal->license->licenseKey ) {
return;
}
aioseoBrokenLinkChecker()->actionScheduler->scheduleRecurrent( $this->actionName, DAY_IN_SECONDS, DAY_IN_SECONDS );
}
/**
* Validates the stored license and resets license data if no key is present.
*
* @since 1.0.0
*
* @return void
*/
public function checkLicense() {
if ( ! $this->internalOptions->internal->license->licenseKey ) {
if ( $this->needsReset() ) {
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level'
]
);
}
return;
}
$this->activateProgrammatic();
}
/**
* Activates the license in response to a direct user action, e.g. connecting their account in the UI.
* Unlike activateProgrammatic(), this is never rate-limited so the user gets immediate feedback.
*
* @since 1.2.9
*
* @return bool Whether or not the license was activated.
*/
public function activateManual() {
return $this->activate();
}
/**
* Activates the license from an automated/background context, e.g. a scheduled check or a quota-change triggered update.
* Rate-limited to one request per hour to prevent flooding the licensing server.
*
* @since 1.2.9
*
* @return bool Whether or not the license was activated.
*/
public function activateProgrammatic() {
if ( aioseoBrokenLinkChecker()->core->cache->get( 'license_programmatic_activation' ) ) {
return false;
}
aioseoBrokenLinkChecker()->core->cache->update( 'license_programmatic_activation', true, HOUR_IN_SECONDS );
return $this->activate();
}
/**
* Activate/validate the license.
* This method should never be called directly. Use activateManual() or activateProgrammatic() instead.
*
* @since 1.0.0
*
* @return bool Whether or not it was activated.
*/
private function activate() {
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level'
]
);
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return false;
}
$site = aioseoBrokenLinkChecker()->helpers->getSite();
$domains = [
'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;
return false;
}
if ( ! empty( $response->error ) ) {
if ( 'missing-key-or-domain' === $response->error ) {
$this->internalOptions->internal->license->requestError = true;
return false;
}
if ( 'missing-license' === $response->error ) {
$this->internalOptions->internal->license->invalid = true;
return false;
}
if ( 'disabled' === $response->error ) {
$this->internalOptions->internal->license->disabled = true;
return false;
}
if ( 'activations' === $response->error ) {
$this->internalOptions->internal->license->activationsError = true;
return false;
}
if ( 'expired' === $response->error ) {
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->expired = true;
return false;
}
}
// Something bad happened, error unknown.
if ( empty( $response->success ) || empty( $response->level ) || ! isset( $response->broken_links_count ) ) {
return false;
}
$oldQuota = $this->internalOptions->internal->license->quota;
$this->internalOptions->internal->license->level = $response->level;
$this->internalOptions->internal->license->expires = strtotime( $response->expires );
$this->internalOptions->internal->license->quota = intval( $response->broken_links_count );
// Set the remaining quota if it's never been set or if the user's plan has changed.
if (
! $this->internalOptions->internal->license->quotaRemaining ||
( intval( $response->broken_links_count ) !== (int) $oldQuota )
) {
$this->internalOptions->internal->license->quotaRemaining = intval( $response->broken_links_count );
}
return true;
}
/**
* Deactivate the license key.
*
* @since 1.0.0
*
* @return bool Whether or not it was deactivated.
*/
public function deactivate() {
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return false;
}
$site = aioseoBrokenLinkChecker()->helpers->getSite();
$domains = [
'domain' => $site->domain,
'path' => $site->path
];
$response = $this->sendLicenseRequest( 'deactivate', $licenseKey, [ $domains ] );
if ( empty( $response ) ) {
// Something bad happened, error unknown.
$this->internalOptions->internal->license->connectionError = true;
return false;
}
if ( ! empty( $response->error ) ) {
if ( 'missing-key-or-domain' === $response->error || 'not-activated' === $response->error ) {
$this->internalOptions->internal->license->requestError = true;
return false;
}
if ( 'missing-license' === $response->error ) {
$this->internalOptions->internal->license->invalid = true;
return false;
}
if ( 'disabled' === $response->error ) {
$this->internalOptions->internal->license->disabled = true;
return false;
}
}
$this->internalOptions->internal->license->reset(
[
'expires',
'expired',
'invalid',
'disabled',
'activationsError',
'connectionError',
'requestError',
'level'
]
);
// Cancel all Link Status scans.
as_unschedule_all_actions( aioseoBrokenLinkChecker()->main->linkStatus->actionName );
return true;
}
/**
* Returns the URL to check licenses.
*
* @since 1.0.0
*
* @return string The URL.
*/
public function getUrl() {
if ( defined( 'AIOSEO_BROKEN_LINK_CHECKER_LICENSING_URL' ) ) {
return AIOSEO_BROKEN_LINK_CHECKER_LICENSING_URL;
}
return $this->baseUrl;
}
/**
* Checks to see if the current license is expired.
*
* @since 1.0.0
*
* @return bool Whether the license is expired.
*/
public function isExpired() {
$networkIsExpired = false;
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return $networkIsExpired;
}
$expired = $this->internalOptions->internal->license->expired || $this->internalOptions->internal->license->expires < time();
if ( $expired ) {
$didActivationAttempt = $this->maybeReactivateExpiredLicense();
// If we tried to activate the license again, start over. Otherwise, return true.
return $didActivationAttempt ? $this->isExpired() : true;
}
$expires = $this->internalOptions->internal->license->expires;
return 0 !== $expires && $expires < time();
}
/**
* Checks to see if the current license is disabled.
*
* @since 1.0.0
*
* @return bool Whether the license is disabled.
*/
public function isDisabled() {
$networkIsDisabled = false;
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return $networkIsDisabled;
}
return $this->internalOptions->internal->license->disabled;
}
/**
* Checks to see if the current license is invalid.
*
* @since 1.0.0
*
* @return bool Whether the license is invalid.
*/
public function isInvalid() {
$networkIsInvalid = false;
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return $networkIsInvalid;
}
return $this->internalOptions->internal->license->invalid;
}
/**
* Checks to see if the current license is active.
*
* @since 1.0.0
*
* @return bool Whether the license is active.
*/
public function isActive() {
$networkIsActive = false;
$licenseKey = $this->internalOptions->internal->license->licenseKey;
if ( empty( $licenseKey ) ) {
return $networkIsActive;
}
return ! $this->isExpired() && ! $this->isDisabled() && ! $this->isInvalid();
}
/**
* Get the license level for the activated license.
*
* @since 1.0.0
*
* @return string The license level.
*/
public function getLicenseLevel() {
return $this->internalOptions->internal->license->level;
}
/**
* Checks if the license data needs to be reset.
*
* @since 1.0.0
*
* @return bool Whether the license data needs to be reet.
*/
private function needsReset() {
if ( ! empty( $this->internalOptions->internal->license->licenseKey ) ) {
return false;
}
if ( $this->internalOptions->internal->license->level ) {
return true;
}
if ( $this->internalOptions->internal->license->invalid ) {
return true;
}
if ( $this->internalOptions->internal->license->disabled ) {
return true;
}
$expired = $this->internalOptions->internal->license->expired;
if ( $expired ) {
return true;
}
$expires = $this->internalOptions->internal->license->expires;
return 0 !== $expires;
}
/**
* Sends the license request.
*
* @since 1.0.0
*
* @param string $type The type of request, either activate or deactivate.
* @param string $licenseKey The license key we are using for this request.
* @param array $domains List of domains to activate or deactivate.
* @return Object|null The JSON response as an object.
*/
public function sendLicenseRequest( $type, $licenseKey, $domains ) {
$payload = [
'sku' => 'aioseo-broken-link-checker',
'version' => AIOSEO_BROKEN_LINK_CHECKER_VERSION,
'php_version' => PHP_VERSION,
'license' => $licenseKey,
'domains' => $domains,
'wp_version' => get_bloginfo( 'version' )
];
$response = aioseoBrokenLinkChecker()->helpers->wpRemotePost( $this->getUrl() . $type . '/', [
'timeout' => 20,
'body' => wp_json_encode( $payload )
] );
$responseBody = wp_remote_retrieve_body( $response );
return ! empty( $responseBody ) ? json_decode( $responseBody ) : null;
}
/**
* Checks if the current site is licensed at the network level.
*
* @since 1.0.0
*
* @return bool Whether the site is licensed at the network level.
*/
public function isNetworkLicensed() {
if ( ! property_exists( aioseoBrokenLinkChecker(), 'networkLicense' ) ) {
return false;
}
return aioseoBrokenLinkChecker()->networkLicense->isActive();
}
/**
* Whether the current license plan is the free plan.
*
* @since 1.0.0
*
* @return bool
*/
public function isFree() {
return 'free' === strtolower( (string) $this->getLicenseLevel() );
}
/**
* Checks if the license is expired and attempts to activate it again.
*
* @since 1.1.0
*
* @return bool True if an attempt was made to activate the license, false if not.
*/
private function maybeReactivateExpiredLicense() {
// If the license is expired, send out a request to check if it's still expired.
// We cache this for a few hours so we don't spam the server.
$transientName = 'expired_license_check';
if ( aioseoBrokenLinkChecker()->core->cache->get( $transientName ) ) {
return false;
}
$this->activateProgrammatic();
aioseoBrokenLinkChecker()->core->cache->update( $transientName, true, 12 * HOUR_IN_SECONDS );
return true;
}
}