You've already forked wc-licensed-product
Implement self-licensing (v0.3.0) and settings sub-tabs (v0.3.1)
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>
This commit is contained in:
287
src/License/PluginLicenseChecker.php
Normal file
287
src/License/PluginLicenseChecker.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user