Initial plugin setup (v0.0.1)
All checks were successful
Create Release Package / build-release (push) Successful in 1m21s
All checks were successful
Create Release Package / build-release (push) Successful in 1m21s
- Main plugin file with PHP 8.3+ and WordPress 6.0+ version checks - Plugin singleton class with admin menu and settings pages - License Manager integration with SecureLicenseClient - License settings tab with validation and activation - Admin CSS and JavaScript for license management - Gitea CI/CD workflow for automated releases - Documentation: README.md, PLAN.md, CHANGELOG.md, CLAUDE.md - Composer dependencies: Twig 3.0, license client - Git submodule for wc-licensed-product-client library Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
533
src/License/Manager.php
Normal file
533
src/License/Manager.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
/**
|
||||
* License Manager class.
|
||||
*
|
||||
* @package Magdev\WpBnb\License
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\License;
|
||||
|
||||
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||
use Magdev\WcLicensedProductClient\DTO\LicenseState;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseNotFoundException;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseExpiredException;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseRevokedException;
|
||||
use Magdev\WcLicensedProductClient\Exception\LicenseInactiveException;
|
||||
use Magdev\WcLicensedProductClient\Exception\DomainMismatchException;
|
||||
use Magdev\WcLicensedProductClient\Exception\MaxActivationsReachedException;
|
||||
use Magdev\WcLicensedProductClient\Exception\RateLimitExceededException;
|
||||
use Magdev\WcLicensedProductClient\Security\SignatureException;
|
||||
use Symfony\Component\HttpClient\HttpClient;
|
||||
|
||||
/**
|
||||
* Manages license validation and activation.
|
||||
*/
|
||||
final class Manager {
|
||||
|
||||
// Option keys.
|
||||
public const OPTION_LICENSE_KEY = 'wp_bnb_license_key';
|
||||
public const OPTION_SERVER_URL = 'wp_bnb_license_server_url';
|
||||
public const OPTION_SERVER_SECRET = 'wp_bnb_license_server_secret';
|
||||
public const OPTION_LICENSE_STATUS = 'wp_bnb_license_status';
|
||||
public const OPTION_LICENSE_DATA = 'wp_bnb_license_data';
|
||||
public const OPTION_LAST_CHECK = 'wp_bnb_license_last_check';
|
||||
|
||||
// Transient keys.
|
||||
private const TRANSIENT_LICENSE_CHECK = 'wp_bnb_license_check';
|
||||
|
||||
// Cache TTL (24 hours).
|
||||
private const CACHE_TTL = 86400;
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var Manager|null
|
||||
*/
|
||||
private static ?Manager $instance = null;
|
||||
|
||||
/**
|
||||
* License client.
|
||||
*
|
||||
* @var SecureLicenseClient|LicenseClient|null
|
||||
*/
|
||||
private SecureLicenseClient|LicenseClient|null $client = null;
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return Manager
|
||||
*/
|
||||
public static function get_instance(): Manager {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// AJAX handlers.
|
||||
add_action( 'wp_ajax_wp_bnb_validate_license', array( $this, 'ajax_validate_license' ) );
|
||||
add_action( 'wp_ajax_wp_bnb_activate_license', array( $this, 'ajax_activate_license' ) );
|
||||
add_action( 'wp_ajax_wp_bnb_check_status', array( $this, 'ajax_check_status' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the license client.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function init_client(): bool {
|
||||
if ( null !== $this->client ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$server_url = self::get_server_url();
|
||||
$server_secret = self::get_server_secret();
|
||||
|
||||
if ( empty( $server_url ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use SecureLicenseClient if server secret is configured.
|
||||
if ( ! empty( $server_secret ) ) {
|
||||
$this->client = new SecureLicenseClient(
|
||||
httpClient: HttpClient::create(),
|
||||
baseUrl: $server_url,
|
||||
serverSecret: $server_secret,
|
||||
);
|
||||
} else {
|
||||
$this->client = new LicenseClient(
|
||||
httpClient: HttpClient::create(),
|
||||
baseUrl: $server_url,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch ( \Throwable $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license is valid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_license_valid(): bool {
|
||||
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||
return 'valid' === $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_license_key(): string {
|
||||
return get_option( self::OPTION_LICENSE_KEY, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_server_url(): string {
|
||||
return get_option( self::OPTION_SERVER_URL, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server secret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_server_secret(): string {
|
||||
return get_option( self::OPTION_SERVER_SECRET, '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached license status.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_cached_status(): string {
|
||||
$server_url = self::get_server_url();
|
||||
$license_key = self::get_license_key();
|
||||
|
||||
if ( empty( $server_url ) || empty( $license_key ) ) {
|
||||
return 'unconfigured';
|
||||
}
|
||||
|
||||
return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached license data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_cached_data(): array {
|
||||
return get_option( self::OPTION_LICENSE_DATA, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last check timestamp.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_last_check(): int {
|
||||
return (int) get_option( self::OPTION_LAST_CHECK, 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save license settings.
|
||||
*
|
||||
* @param array $data Settings data.
|
||||
* @return bool
|
||||
*/
|
||||
public static function save_settings( array $data ): bool {
|
||||
if ( isset( $data['license_key'] ) ) {
|
||||
update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $data['server_url'] ) ) {
|
||||
update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) );
|
||||
}
|
||||
|
||||
// Only update secret if provided (not empty).
|
||||
if ( ! empty( $data['server_secret'] ) ) {
|
||||
update_option( self::OPTION_SERVER_SECRET, sanitize_text_field( $data['server_secret'] ) );
|
||||
}
|
||||
|
||||
// Clear cached status when settings change.
|
||||
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all license data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear_license_data(): void {
|
||||
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
|
||||
update_option( self::OPTION_LICENSE_DATA, array() );
|
||||
update_option( self::OPTION_LAST_CHECK, 0 );
|
||||
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate license.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate(): array {
|
||||
if ( ! $this->init_client() ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'License client could not be initialized. Please check server URL and secret.', 'wp-bnb' ),
|
||||
'status' => 'unconfigured',
|
||||
);
|
||||
}
|
||||
|
||||
$license_key = self::get_license_key();
|
||||
if ( empty( $license_key ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Please enter a license key.', 'wp-bnb' ),
|
||||
'status' => 'unconfigured',
|
||||
);
|
||||
}
|
||||
|
||||
$domain = $this->get_current_domain();
|
||||
|
||||
try {
|
||||
$license_info = $this->client->validate( $license_key, $domain );
|
||||
|
||||
// Store license data.
|
||||
$license_data = array(
|
||||
'product_id' => $license_info->productId,
|
||||
'expires' => $license_info->expiresAt?->format( 'Y-m-d H:i:s' ),
|
||||
);
|
||||
|
||||
update_option( self::OPTION_LICENSE_STATUS, 'valid' );
|
||||
update_option( self::OPTION_LICENSE_DATA, $license_data );
|
||||
update_option( self::OPTION_LAST_CHECK, time() );
|
||||
set_transient( self::TRANSIENT_LICENSE_CHECK, 'valid', self::CACHE_TTL );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'License is valid.', 'wp-bnb' ),
|
||||
'status' => 'valid',
|
||||
'data' => $license_data,
|
||||
);
|
||||
} catch ( LicenseNotFoundException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'License key not found.', 'wp-bnb' ) );
|
||||
} catch ( LicenseExpiredException $e ) {
|
||||
return $this->handle_license_error( 'expired', __( 'License has expired.', 'wp-bnb' ) );
|
||||
} catch ( LicenseRevokedException $e ) {
|
||||
return $this->handle_license_error( 'revoked', __( 'License has been revoked.', 'wp-bnb' ) );
|
||||
} catch ( LicenseInactiveException $e ) {
|
||||
return $this->handle_license_error( 'inactive', __( 'License is inactive. Please activate it first.', 'wp-bnb' ) );
|
||||
} catch ( DomainMismatchException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'License is not valid for this domain.', 'wp-bnb' ) );
|
||||
} catch ( SignatureException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'Response signature verification failed. Please check server secret.', 'wp-bnb' ) );
|
||||
} catch ( RateLimitExceededException $e ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %d: Seconds to wait */
|
||||
__( 'Rate limit exceeded. Please try again in %d seconds.', 'wp-bnb' ),
|
||||
$e->retryAfter
|
||||
),
|
||||
'status' => self::get_cached_status(),
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'An error occurred while validating the license.', 'wp-bnb' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate license.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function activate(): array {
|
||||
if ( ! $this->init_client() ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'License client could not be initialized. Please check server URL and secret.', 'wp-bnb' ),
|
||||
'status' => 'unconfigured',
|
||||
);
|
||||
}
|
||||
|
||||
$license_key = self::get_license_key();
|
||||
if ( empty( $license_key ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Please enter a license key.', 'wp-bnb' ),
|
||||
'status' => 'unconfigured',
|
||||
);
|
||||
}
|
||||
|
||||
$domain = $this->get_current_domain();
|
||||
|
||||
try {
|
||||
$result = $this->client->activate( $license_key, $domain );
|
||||
|
||||
if ( $result->success ) {
|
||||
// Validate after activation to get full license info.
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __( 'Activation failed.', 'wp-bnb' ),
|
||||
'status' => 'inactive',
|
||||
);
|
||||
} catch ( MaxActivationsReachedException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'Maximum activations reached for this license.', 'wp-bnb' ) );
|
||||
} catch ( LicenseNotFoundException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'License key not found.', 'wp-bnb' ) );
|
||||
} catch ( LicenseExpiredException $e ) {
|
||||
return $this->handle_license_error( 'expired', __( 'License has expired.', 'wp-bnb' ) );
|
||||
} catch ( SignatureException $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'Response signature verification failed. Please check server secret.', 'wp-bnb' ) );
|
||||
} catch ( RateLimitExceededException $e ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %d: Seconds to wait */
|
||||
__( 'Rate limit exceeded. Please try again in %d seconds.', 'wp-bnb' ),
|
||||
$e->retryAfter
|
||||
),
|
||||
'status' => self::get_cached_status(),
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
return $this->handle_license_error( 'invalid', __( 'An error occurred while activating the license.', 'wp-bnb' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license status.
|
||||
*
|
||||
* @param bool $force_refresh Whether to force a refresh from server.
|
||||
* @return array
|
||||
*/
|
||||
public function get_status( bool $force_refresh = false ): array {
|
||||
// Check transient cache first.
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||
if ( false !== $cached ) {
|
||||
return array(
|
||||
'success' => 'valid' === $cached,
|
||||
'status' => $cached,
|
||||
'data' => self::get_cached_data(),
|
||||
'cached' => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->init_client() ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'status' => 'unconfigured',
|
||||
'message' => __( 'License client not configured.', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
|
||||
$license_key = self::get_license_key();
|
||||
if ( empty( $license_key ) ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'status' => 'unconfigured',
|
||||
'message' => __( 'No license key configured.', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$status_info = $this->client->status( $license_key );
|
||||
|
||||
$status = match ( $status_info->status ) {
|
||||
LicenseState::Active => 'valid',
|
||||
LicenseState::Inactive => 'inactive',
|
||||
LicenseState::Expired => 'expired',
|
||||
LicenseState::Revoked => 'revoked',
|
||||
};
|
||||
|
||||
update_option( self::OPTION_LICENSE_STATUS, $status );
|
||||
update_option( self::OPTION_LAST_CHECK, time() );
|
||||
set_transient( self::TRANSIENT_LICENSE_CHECK, $status, self::CACHE_TTL );
|
||||
|
||||
return array(
|
||||
'success' => 'valid' === $status,
|
||||
'status' => $status,
|
||||
'data' => self::get_cached_data(),
|
||||
'cached' => false,
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'status' => 'invalid',
|
||||
'message' => __( 'Failed to check license status.', 'wp-bnb' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle license error.
|
||||
*
|
||||
* @param string $status Status code.
|
||||
* @param string $message Error message.
|
||||
* @return array
|
||||
*/
|
||||
private function handle_license_error( string $status, string $message ): array {
|
||||
update_option( self::OPTION_LICENSE_STATUS, $status );
|
||||
update_option( self::OPTION_LAST_CHECK, time() );
|
||||
delete_transient( self::TRANSIENT_LICENSE_CHECK );
|
||||
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current domain.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_current_domain(): string {
|
||||
$site_url = get_site_url();
|
||||
$parsed = wp_parse_url( $site_url );
|
||||
return $parsed['host'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Validate license.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_validate_license(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array(
|
||||
'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ),
|
||||
) );
|
||||
}
|
||||
|
||||
$result = $this->validate();
|
||||
|
||||
if ( $result['success'] ) {
|
||||
wp_send_json_success( $result );
|
||||
} else {
|
||||
wp_send_json_error( $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Activate license.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_activate_license(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array(
|
||||
'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ),
|
||||
) );
|
||||
}
|
||||
|
||||
$result = $this->activate();
|
||||
|
||||
if ( $result['success'] ) {
|
||||
wp_send_json_success( $result );
|
||||
} else {
|
||||
wp_send_json_error( $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Check status.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_check_status(): void {
|
||||
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array(
|
||||
'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ),
|
||||
) );
|
||||
}
|
||||
|
||||
$force_refresh = isset( $_POST['force'] ) && 'true' === $_POST['force'];
|
||||
$result = $this->get_status( $force_refresh );
|
||||
|
||||
if ( $result['success'] ) {
|
||||
wp_send_json_success( $result );
|
||||
} else {
|
||||
wp_send_json_error( $result );
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user