Files
wc-licensed-product/src/License/PluginLicenseChecker.php
magdev 4817175f99 Add self-licensing prevention to PluginLicenseChecker
- Add isSelfLicensing() method to detect when license server URL points to same installation
- Bypass license validation when self-licensing detected (prevents circular dependency)
- Add normalizeDomain() helper for domain comparison
- Update translations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 16:41:56 +01:00

358 lines
9.6 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;
/**
* Cached self-licensing check result
*/
private ?bool $isSelfLicensingCached = 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;
}
// Always valid when self-licensing (server URL points to this installation)
if ($this->isSelfLicensing()) {
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;
}
// Always valid when self-licensing (server URL points to this installation)
if ($this->isSelfLicensing()) {
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;
$this->isSelfLicensingCached = 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;
}
/**
* Check if self-licensing (license server URL points to this installation)
*
* Prevents circular dependency where plugin tries to validate against itself.
* Plugins can only be validated against the original store from which they were obtained.
*/
public function isSelfLicensing(): bool
{
if ($this->isSelfLicensingCached !== null) {
return $this->isSelfLicensingCached;
}
$serverUrl = $this->getLicenseServerUrl();
// No server URL configured - not self-licensing
if (empty($serverUrl)) {
$this->isSelfLicensingCached = false;
return false;
}
// Parse both URLs to compare domains
$serverParsed = parse_url($serverUrl);
$siteUrl = get_site_url();
$siteParsed = parse_url($siteUrl);
// Get normalized domains (lowercase, no www prefix)
$serverDomain = $this->normalizeDomain($serverParsed['host'] ?? '');
$siteDomain = $this->normalizeDomain($siteParsed['host'] ?? '');
// If domains match, this is self-licensing
if ($serverDomain === $siteDomain) {
$this->isSelfLicensingCached = true;
return true;
}
$this->isSelfLicensingCached = false;
return false;
}
/**
* Normalize a domain for comparison (lowercase, strip www)
*/
private function normalizeDomain(string $domain): string
{
$domain = strtolower(trim($domain));
// Strip www. prefix
if (str_starts_with($domain, 'www.')) {
$domain = substr($domain, 4);
}
return $domain;
}
/**
* 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,
);
}
}