You've already forked wc-licensed-product
v0.3.0 - Self-Licensing: - Add PluginLicenseChecker singleton for license validation - Integrate magdev/wc-licensed-product-client library - Add license settings: server URL, key, optional secret - Disable frontend features without valid license (except localhost) - Add license status display with verify button in settings v0.3.1 - Settings UI Improvements: - Reorganize settings page with WooCommerce-style sub-tabs - Split settings into: Plugin License, Default Settings, Notifications - Use PHP 8 match expression for section-specific rendering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
288 lines
7.5 KiB
PHP
288 lines
7.5 KiB
PHP
<?php
|
|
/**
|
|
* Plugin License Checker
|
|
*
|
|
* Validates the plugin's own license against a remote server.
|
|
*
|
|
* @package Jeremias\WcLicensedProduct\License
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Jeremias\WcLicensedProduct\License;
|
|
|
|
use Magdev\WcLicensedProductClient\LicenseClient;
|
|
use Magdev\WcLicensedProductClient\LicenseClientInterface;
|
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
|
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
|
use Symfony\Component\HttpClient\HttpClient;
|
|
|
|
/**
|
|
* Handles validation of this plugin's license
|
|
*/
|
|
final class PluginLicenseChecker
|
|
{
|
|
/**
|
|
* Cache key for license validation result
|
|
*/
|
|
private const CACHE_KEY = 'wclp_plugin_license_valid';
|
|
|
|
/**
|
|
* Cache TTL for successful validation (1 hour)
|
|
*/
|
|
private const CACHE_TTL = 3600;
|
|
|
|
/**
|
|
* Cache key for error messages
|
|
*/
|
|
private const ERROR_CACHE_KEY = 'wclp_plugin_license_error';
|
|
|
|
/**
|
|
* Cache TTL for errors (5 minutes)
|
|
*/
|
|
private const ERROR_CACHE_TTL = 300;
|
|
|
|
/**
|
|
* Singleton instance
|
|
*/
|
|
private static ?self $instance = null;
|
|
|
|
/**
|
|
* Cached localhost check result
|
|
*/
|
|
private ?bool $isLocalhostCached = null;
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static function getInstance(): self
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Private constructor for singleton
|
|
*/
|
|
private function __construct()
|
|
{
|
|
// Private constructor
|
|
}
|
|
|
|
/**
|
|
* Check if the plugin license is valid
|
|
*
|
|
* Returns cached result if available, otherwise validates against server.
|
|
*/
|
|
public function isLicenseValid(): bool
|
|
{
|
|
// Always valid on localhost
|
|
if ($this->isLocalhost()) {
|
|
return true;
|
|
}
|
|
|
|
// Check cache first
|
|
$cached = get_transient(self::CACHE_KEY);
|
|
if ($cached !== false) {
|
|
return (bool) $cached;
|
|
}
|
|
|
|
// Validate against server
|
|
return $this->validateLicense();
|
|
}
|
|
|
|
/**
|
|
* Validate license against the server
|
|
*
|
|
* @param bool $forceRefresh Force refresh even if cached
|
|
* @return bool True if license is valid
|
|
*/
|
|
public function validateLicense(bool $forceRefresh = false): bool
|
|
{
|
|
// Always valid on localhost
|
|
if ($this->isLocalhost()) {
|
|
return true;
|
|
}
|
|
|
|
// Check settings are configured
|
|
$serverUrl = $this->getLicenseServerUrl();
|
|
$licenseKey = $this->getLicenseKey();
|
|
|
|
if (empty($serverUrl) || empty($licenseKey)) {
|
|
set_transient(
|
|
self::ERROR_CACHE_KEY,
|
|
__('License settings not configured.', 'wc-licensed-product'),
|
|
self::ERROR_CACHE_TTL
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Check cache unless force refresh
|
|
if (!$forceRefresh) {
|
|
$cached = get_transient(self::CACHE_KEY);
|
|
if ($cached !== false) {
|
|
return (bool) $cached;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$client = $this->createLicenseClient();
|
|
$domain = $this->getCurrentDomain();
|
|
|
|
// Validate the license
|
|
$client->validate($licenseKey, $domain);
|
|
|
|
// Valid license - cache success
|
|
set_transient(self::CACHE_KEY, 1, self::CACHE_TTL);
|
|
delete_transient(self::ERROR_CACHE_KEY);
|
|
|
|
return true;
|
|
} catch (LicenseException $e) {
|
|
// License-specific error (invalid, expired, revoked, etc.)
|
|
set_transient(self::CACHE_KEY, 0, self::CACHE_TTL);
|
|
set_transient(self::ERROR_CACHE_KEY, $e->getMessage(), self::ERROR_CACHE_TTL);
|
|
|
|
return false;
|
|
} catch (\Throwable $e) {
|
|
// Network/server error - use shorter cache to allow retry
|
|
set_transient(
|
|
self::ERROR_CACHE_KEY,
|
|
__('Could not connect to license server.', 'wc-licensed-product') . ' ' . $e->getMessage(),
|
|
self::ERROR_CACHE_TTL
|
|
);
|
|
|
|
// Don't cache validation failure on network errors - allow retry on next page load
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the last error message
|
|
*/
|
|
public function getLastError(): ?string
|
|
{
|
|
$error = get_transient(self::ERROR_CACHE_KEY);
|
|
return $error !== false ? (string) $error : null;
|
|
}
|
|
|
|
/**
|
|
* Clear the validation cache
|
|
*/
|
|
public function clearCache(): void
|
|
{
|
|
delete_transient(self::CACHE_KEY);
|
|
delete_transient(self::ERROR_CACHE_KEY);
|
|
$this->isLocalhostCached = null;
|
|
}
|
|
|
|
/**
|
|
* Check if running on localhost
|
|
*
|
|
* Matches localhost, 127.0.0.1, ::1, and any port number.
|
|
*/
|
|
public function isLocalhost(): bool
|
|
{
|
|
if ($this->isLocalhostCached !== null) {
|
|
return $this->isLocalhostCached;
|
|
}
|
|
|
|
$domain = $this->getCurrentDomain();
|
|
|
|
// Remove port number if present
|
|
$domainWithoutPort = preg_replace('/:[\d]+$/', '', $domain);
|
|
|
|
// Check for localhost variants
|
|
$localhostNames = ['localhost', '127.0.0.1', '::1'];
|
|
|
|
if (in_array($domainWithoutPort, $localhostNames, true)) {
|
|
$this->isLocalhostCached = true;
|
|
return true;
|
|
}
|
|
|
|
// Check for .localhost and .local subdomains
|
|
if (
|
|
str_ends_with($domainWithoutPort, '.localhost') ||
|
|
str_ends_with($domainWithoutPort, '.local')
|
|
) {
|
|
$this->isLocalhostCached = true;
|
|
return true;
|
|
}
|
|
|
|
$this->isLocalhostCached = false;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the current domain from the site URL
|
|
*/
|
|
private function getCurrentDomain(): string
|
|
{
|
|
$siteUrl = get_site_url();
|
|
$parsed = parse_url($siteUrl);
|
|
$host = $parsed['host'] ?? 'localhost';
|
|
|
|
// Include port if non-standard
|
|
if (isset($parsed['port'])) {
|
|
$host .= ':' . $parsed['port'];
|
|
}
|
|
|
|
return strtolower($host);
|
|
}
|
|
|
|
/**
|
|
* Get the license server URL from settings
|
|
*/
|
|
private function getLicenseServerUrl(): string
|
|
{
|
|
return (string) get_option('wc_licensed_product_plugin_license_server_url', '');
|
|
}
|
|
|
|
/**
|
|
* Get the license key from settings
|
|
*/
|
|
private function getLicenseKey(): string
|
|
{
|
|
return (string) get_option('wc_licensed_product_plugin_license_key', '');
|
|
}
|
|
|
|
/**
|
|
* Get the server secret from settings (optional)
|
|
*/
|
|
private function getServerSecret(): ?string
|
|
{
|
|
$secret = get_option('wc_licensed_product_plugin_license_server_secret', '');
|
|
return !empty($secret) ? (string) $secret : null;
|
|
}
|
|
|
|
/**
|
|
* Create the license client instance
|
|
*/
|
|
private function createLicenseClient(): LicenseClientInterface
|
|
{
|
|
$httpClient = HttpClient::create([
|
|
'timeout' => 10,
|
|
'verify_peer' => true,
|
|
]);
|
|
|
|
$serverUrl = $this->getLicenseServerUrl();
|
|
$serverSecret = $this->getServerSecret();
|
|
|
|
// Use secure client if server secret is configured
|
|
if ($serverSecret !== null) {
|
|
return new SecureLicenseClient(
|
|
httpClient: $httpClient,
|
|
baseUrl: $serverUrl,
|
|
serverSecret: $serverSecret,
|
|
);
|
|
}
|
|
|
|
return new LicenseClient(
|
|
httpClient: $httpClient,
|
|
baseUrl: $serverUrl,
|
|
);
|
|
}
|
|
}
|