You've already forked wc-tier-and-package-prices
Version 1.4.1 - Localhost license bypass and auto-updates
All checks were successful
Create Release Package / build-release (push) Successful in 1m3s
All checks were successful
Create Release Package / build-release (push) Successful in 1m3s
Added localhost/self-licensing license bypass and WordPress auto-update integration from license server. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
333
includes/class-wc-tpp-license-checker.php
Normal file
333
includes/class-wc-tpp-license-checker.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user