You've already forked wc-tier-and-package-prices
334 lines
9.7 KiB
PHP
334 lines
9.7 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* License Checker
|
||
|
|
*
|
||
|
|
* Handles license validation with localhost and self-licensing bypass
|
||
|
|
*
|
||
|
|
* @package WC_Tier_Package_Prices
|
||
|
|
*/
|
||
|
|
|
||
|
|
if (!defined('ABSPATH')) {
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* WC_TPP_License_Checker class
|
||
|
|
*/
|
||
|
|
if (!class_exists('WC_TPP_License_Checker')) {
|
||
|
|
class WC_TPP_License_Checker {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Singleton instance
|
||
|
|
*
|
||
|
|
* @var WC_TPP_License_Checker|null
|
||
|
|
*/
|
||
|
|
private static $instance = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache key for license status
|
||
|
|
*/
|
||
|
|
private const CACHE_KEY = 'wc_tpp_license_status';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache TTL for successful validation (1 hour)
|
||
|
|
*/
|
||
|
|
private const CACHE_TTL_SUCCESS = 3600;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache TTL for failed validation (5 minutes)
|
||
|
|
*/
|
||
|
|
private const CACHE_TTL_ERROR = 300;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Localhost patterns
|
||
|
|
*
|
||
|
|
* @var array
|
||
|
|
*/
|
||
|
|
private const LOCALHOST_HOSTS = ['localhost', '127.0.0.1', '::1'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Localhost TLDs
|
||
|
|
*
|
||
|
|
* @var array
|
||
|
|
*/
|
||
|
|
private const LOCALHOST_TLDS = ['.localhost', '.local', '.test', '.example', '.invalid'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get singleton instance
|
||
|
|
*
|
||
|
|
* @return WC_TPP_License_Checker
|
||
|
|
*/
|
||
|
|
public static function get_instance(): WC_TPP_License_Checker {
|
||
|
|
if (null === self::$instance) {
|
||
|
|
self::$instance = new self();
|
||
|
|
}
|
||
|
|
return self::$instance;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Constructor
|
||
|
|
*/
|
||
|
|
private function __construct() {
|
||
|
|
// Clear cache when license settings change
|
||
|
|
add_action('update_option_wc_tpp_license_key', array($this, 'clear_cache'));
|
||
|
|
add_action('update_option_wc_tpp_license_server_url', array($this, 'clear_cache'));
|
||
|
|
add_action('update_option_wc_tpp_license_server_secret', array($this, 'clear_cache'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if the current environment is localhost
|
||
|
|
*
|
||
|
|
* Matches patterns:
|
||
|
|
* - localhost
|
||
|
|
* - 127.0.0.1
|
||
|
|
* - ::1 (IPv6 localhost)
|
||
|
|
* - *.localhost (subdomains)
|
||
|
|
* - *.local (subdomains)
|
||
|
|
* - *.test (RFC 2606)
|
||
|
|
* - *.example (RFC 2606)
|
||
|
|
* - *.invalid (RFC 2606)
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function is_localhost(): bool {
|
||
|
|
$domain = $this->get_current_domain();
|
||
|
|
|
||
|
|
// Remove port number if present
|
||
|
|
$domain = preg_replace('/:[\d]+$/', '', $domain);
|
||
|
|
$domain = strtolower($domain);
|
||
|
|
|
||
|
|
// Check exact matches
|
||
|
|
if (in_array($domain, self::LOCALHOST_HOSTS, true)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check TLD patterns
|
||
|
|
foreach (self::LOCALHOST_TLDS as $tld) {
|
||
|
|
if (str_ends_with($domain, $tld)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for private IP ranges (Docker, VMs, etc.)
|
||
|
|
if ($this->is_private_ip($domain)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if domain is a private IP address
|
||
|
|
*
|
||
|
|
* @param string $domain The domain to check.
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
private function is_private_ip(string $domain): bool {
|
||
|
|
// Check if it's a valid IP first
|
||
|
|
if (!filter_var($domain, FILTER_VALIDATE_IP)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for private/reserved ranges
|
||
|
|
return !filter_var(
|
||
|
|
$domain,
|
||
|
|
FILTER_VALIDATE_IP,
|
||
|
|
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if the current site is self-licensing
|
||
|
|
*
|
||
|
|
* Returns true if the license server URL and site URL are on the same domain.
|
||
|
|
* This allows the license server itself to use the plugin without a license.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function is_self_licensing(): bool {
|
||
|
|
$server_url = get_option('wc_tpp_license_server_url', '');
|
||
|
|
|
||
|
|
if (empty($server_url)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$server_domain = $this->normalize_domain($server_url);
|
||
|
|
$site_domain = $this->normalize_domain(get_site_url());
|
||
|
|
|
||
|
|
return $server_domain === $site_domain;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Normalize a URL to its domain
|
||
|
|
*
|
||
|
|
* @param string $url The URL to normalize.
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
private function normalize_domain(string $url): string {
|
||
|
|
$parsed = wp_parse_url($url);
|
||
|
|
$host = $parsed['host'] ?? '';
|
||
|
|
|
||
|
|
// Convert to lowercase
|
||
|
|
$host = strtolower($host);
|
||
|
|
|
||
|
|
// Remove www. prefix
|
||
|
|
$host = preg_replace('/^www\./', '', $host);
|
||
|
|
|
||
|
|
return $host;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current domain from site URL
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function get_current_domain(): string {
|
||
|
|
$site_url = get_site_url();
|
||
|
|
$parsed = wp_parse_url($site_url);
|
||
|
|
$host = $parsed['host'] ?? 'localhost';
|
||
|
|
|
||
|
|
// Include port if non-standard
|
||
|
|
if (isset($parsed['port'])) {
|
||
|
|
$host .= ':' . $parsed['port'];
|
||
|
|
}
|
||
|
|
|
||
|
|
return strtolower($host);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if the license is valid
|
||
|
|
*
|
||
|
|
* This is the main entry point for license validation.
|
||
|
|
* It implements the bypass logic for localhost and self-licensing.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function is_license_valid(): bool {
|
||
|
|
// Always valid on localhost
|
||
|
|
if ($this->is_localhost()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Always valid for self-licensing
|
||
|
|
if ($this->is_self_licensing()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check cached status
|
||
|
|
$cached = $this->get_cached_status();
|
||
|
|
if (false !== $cached) {
|
||
|
|
return !empty($cached['valid']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate against server
|
||
|
|
return $this->validate_license();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the license bypass reason if applicable
|
||
|
|
*
|
||
|
|
* @return string|null 'localhost', 'self_licensing', or null
|
||
|
|
*/
|
||
|
|
public function get_bypass_reason(): ?string {
|
||
|
|
if ($this->is_localhost()) {
|
||
|
|
return 'localhost';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->is_self_licensing()) {
|
||
|
|
return 'self_licensing';
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get cached license status
|
||
|
|
*
|
||
|
|
* @return array|false
|
||
|
|
*/
|
||
|
|
public function get_cached_status() {
|
||
|
|
return get_transient(self::CACHE_KEY);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate license against the server
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
private function validate_license(): bool {
|
||
|
|
$license_key = get_option('wc_tpp_license_key', '');
|
||
|
|
$server_url = get_option('wc_tpp_license_server_url', '');
|
||
|
|
$server_secret = get_option('wc_tpp_license_server_secret', '');
|
||
|
|
|
||
|
|
// Can't validate without credentials
|
||
|
|
if (empty($license_key) || empty($server_url) || empty($server_secret)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$client = $this->get_license_client($server_url, $server_secret);
|
||
|
|
$domain = $this->get_current_domain();
|
||
|
|
|
||
|
|
// Remove port for validation
|
||
|
|
$domain = preg_replace('/:[\d]+$/', '', $domain);
|
||
|
|
|
||
|
|
$result = $client->validate($license_key, $domain);
|
||
|
|
|
||
|
|
// Cache successful validation
|
||
|
|
set_transient(self::CACHE_KEY, array(
|
||
|
|
'valid' => true,
|
||
|
|
'product_id' => $result->productId,
|
||
|
|
'expires_at' => $result->expiresAt?->format('Y-m-d H:i:s'),
|
||
|
|
'is_lifetime' => $result->isLifetime(),
|
||
|
|
'checked_at' => current_time('mysql'),
|
||
|
|
), self::CACHE_TTL_SUCCESS);
|
||
|
|
|
||
|
|
return true;
|
||
|
|
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Cache validation failure
|
||
|
|
set_transient(self::CACHE_KEY, array(
|
||
|
|
'valid' => false,
|
||
|
|
'error' => $e->getMessage(),
|
||
|
|
'checked_at' => current_time('mysql'),
|
||
|
|
), self::CACHE_TTL_ERROR);
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get license client instance
|
||
|
|
*
|
||
|
|
* @param string $server_url License server URL.
|
||
|
|
* @param string $server_secret Shared secret for signature verification.
|
||
|
|
* @return \Magdev\WcLicensedProductClient\LicenseClientInterface
|
||
|
|
*/
|
||
|
|
private function get_license_client(string $server_url, string $server_secret): \Magdev\WcLicensedProductClient\LicenseClientInterface {
|
||
|
|
$httpClient = \Symfony\Component\HttpClient\HttpClient::create();
|
||
|
|
return new \Magdev\WcLicensedProductClient\SecureLicenseClient(
|
||
|
|
httpClient: $httpClient,
|
||
|
|
baseUrl: $server_url,
|
||
|
|
serverSecret: $server_secret,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear the license cache
|
||
|
|
*/
|
||
|
|
public function clear_cache(): void {
|
||
|
|
delete_transient(self::CACHE_KEY);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Force revalidation of the license
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public function revalidate(): bool {
|
||
|
|
$this->clear_cache();
|
||
|
|
return $this->is_license_valid();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|