2026-01-21 18:55:18 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* REST API Controller
|
|
|
|
|
*
|
|
|
|
|
* @package Jeremias\WcLicensedProduct\Api
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Jeremias\WcLicensedProduct\Api;
|
|
|
|
|
|
|
|
|
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
|
|
|
|
use WP_REST_Request;
|
|
|
|
|
use WP_REST_Response;
|
|
|
|
|
use WP_REST_Server;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles REST API endpoints for license validation
|
|
|
|
|
*/
|
|
|
|
|
final class RestApiController
|
|
|
|
|
{
|
|
|
|
|
private const NAMESPACE = 'wc-licensed-product/v1';
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
/**
|
|
|
|
|
* Rate limit: requests per minute per IP
|
|
|
|
|
*/
|
|
|
|
|
private const RATE_LIMIT_REQUESTS = 30;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rate limit window in seconds
|
|
|
|
|
*/
|
|
|
|
|
private const RATE_LIMIT_WINDOW = 60;
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
private LicenseManager $licenseManager;
|
|
|
|
|
|
|
|
|
|
public function __construct(LicenseManager $licenseManager)
|
|
|
|
|
{
|
|
|
|
|
$this->licenseManager = $licenseManager;
|
|
|
|
|
$this->registerHooks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register WordPress hooks
|
|
|
|
|
*/
|
|
|
|
|
private function registerHooks(): void
|
|
|
|
|
{
|
|
|
|
|
add_action('rest_api_init', [$this, 'registerRoutes']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
/**
|
|
|
|
|
* Check rate limit for current IP
|
|
|
|
|
*
|
|
|
|
|
* @return WP_REST_Response|null Returns error response if rate limited, null if OK
|
|
|
|
|
*/
|
|
|
|
|
private function checkRateLimit(): ?WP_REST_Response
|
|
|
|
|
{
|
|
|
|
|
$ip = $this->getClientIp();
|
|
|
|
|
$transientKey = 'wclp_rate_' . md5($ip);
|
|
|
|
|
|
|
|
|
|
$data = get_transient($transientKey);
|
|
|
|
|
|
|
|
|
|
if ($data === false) {
|
|
|
|
|
// First request, start counting
|
|
|
|
|
set_transient($transientKey, ['count' => 1, 'start' => time()], self::RATE_LIMIT_WINDOW);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$count = (int) ($data['count'] ?? 0);
|
|
|
|
|
$start = (int) ($data['start'] ?? time());
|
|
|
|
|
|
|
|
|
|
// Check if window has expired
|
|
|
|
|
if (time() - $start >= self::RATE_LIMIT_WINDOW) {
|
|
|
|
|
// Reset counter
|
|
|
|
|
set_transient($transientKey, ['count' => 1, 'start' => time()], self::RATE_LIMIT_WINDOW);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if limit exceeded
|
|
|
|
|
if ($count >= self::RATE_LIMIT_REQUESTS) {
|
|
|
|
|
$retryAfter = self::RATE_LIMIT_WINDOW - (time() - $start);
|
|
|
|
|
$response = new WP_REST_Response([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'rate_limit_exceeded',
|
|
|
|
|
'message' => __('Too many requests. Please try again later.', 'wc-licensed-product'),
|
|
|
|
|
'retry_after' => $retryAfter,
|
|
|
|
|
], 429);
|
|
|
|
|
$response->header('Retry-After', (string) $retryAfter);
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Increment counter
|
|
|
|
|
set_transient($transientKey, ['count' => $count + 1, 'start' => $start], self::RATE_LIMIT_WINDOW);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get client IP address
|
|
|
|
|
*/
|
|
|
|
|
private function getClientIp(): string
|
|
|
|
|
{
|
|
|
|
|
$headers = [
|
|
|
|
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
|
|
|
|
'HTTP_X_FORWARDED_FOR',
|
|
|
|
|
'HTTP_X_REAL_IP',
|
|
|
|
|
'REMOTE_ADDR',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($headers as $header) {
|
|
|
|
|
if (!empty($_SERVER[$header])) {
|
|
|
|
|
$ips = explode(',', $_SERVER[$header]);
|
|
|
|
|
$ip = trim($ips[0]);
|
|
|
|
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
|
|
|
|
return $ip;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '0.0.0.0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* Register REST API routes
|
|
|
|
|
*/
|
|
|
|
|
public function registerRoutes(): void
|
|
|
|
|
{
|
|
|
|
|
// Validate license endpoint (public)
|
|
|
|
|
register_rest_route(self::NAMESPACE, '/validate', [
|
|
|
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
|
|
|
'callback' => [$this, 'validateLicense'],
|
|
|
|
|
'permission_callback' => '__return_true',
|
|
|
|
|
'args' => [
|
|
|
|
|
'license_key' => [
|
|
|
|
|
'required' => true,
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
|
|
|
'validate_callback' => function ($value): bool {
|
|
|
|
|
return !empty($value) && strlen($value) <= 64;
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
'domain' => [
|
|
|
|
|
'required' => true,
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
|
|
|
'validate_callback' => function ($value): bool {
|
|
|
|
|
return !empty($value) && strlen($value) <= 255;
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Check license status endpoint (public)
|
|
|
|
|
register_rest_route(self::NAMESPACE, '/status', [
|
|
|
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
|
|
|
'callback' => [$this, 'checkStatus'],
|
|
|
|
|
'permission_callback' => '__return_true',
|
|
|
|
|
'args' => [
|
|
|
|
|
'license_key' => [
|
|
|
|
|
'required' => true,
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Activate license on domain endpoint (public)
|
|
|
|
|
register_rest_route(self::NAMESPACE, '/activate', [
|
|
|
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
|
|
|
'callback' => [$this, 'activateLicense'],
|
|
|
|
|
'permission_callback' => '__return_true',
|
|
|
|
|
'args' => [
|
|
|
|
|
'license_key' => [
|
|
|
|
|
'required' => true,
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
|
|
|
],
|
|
|
|
|
'domain' => [
|
|
|
|
|
'required' => true,
|
|
|
|
|
'type' => 'string',
|
|
|
|
|
'sanitize_callback' => 'sanitize_text_field',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate license endpoint
|
|
|
|
|
*/
|
|
|
|
|
public function validateLicense(WP_REST_Request $request): WP_REST_Response
|
|
|
|
|
{
|
2026-01-21 19:15:19 +01:00
|
|
|
$rateLimitResponse = $this->checkRateLimit();
|
|
|
|
|
if ($rateLimitResponse !== null) {
|
|
|
|
|
return $rateLimitResponse;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
$licenseKey = $request->get_param('license_key');
|
|
|
|
|
$domain = $request->get_param('domain');
|
|
|
|
|
|
|
|
|
|
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
|
|
|
|
|
|
|
|
|
$statusCode = $result['valid'] ? 200 : 403;
|
|
|
|
|
|
|
|
|
|
return new WP_REST_Response($result, $statusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check license status endpoint
|
|
|
|
|
*/
|
|
|
|
|
public function checkStatus(WP_REST_Request $request): WP_REST_Response
|
|
|
|
|
{
|
2026-01-21 19:15:19 +01:00
|
|
|
$rateLimitResponse = $this->checkRateLimit();
|
|
|
|
|
if ($rateLimitResponse !== null) {
|
|
|
|
|
return $rateLimitResponse;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
$licenseKey = $request->get_param('license_key');
|
|
|
|
|
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
|
|
|
|
|
|
|
|
|
if (!$license) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'valid' => false,
|
|
|
|
|
'error' => 'license_not_found',
|
|
|
|
|
'message' => __('License key not found.', 'wc-licensed-product'),
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'valid' => $license->isValid(),
|
|
|
|
|
'status' => $license->getStatus(),
|
|
|
|
|
'domain' => $license->getDomain(),
|
|
|
|
|
'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
|
|
|
|
|
'activations_count' => $license->getActivationsCount(),
|
|
|
|
|
'max_activations' => $license->getMaxActivations(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Activate license on domain endpoint
|
|
|
|
|
*/
|
|
|
|
|
public function activateLicense(WP_REST_Request $request): WP_REST_Response
|
|
|
|
|
{
|
2026-01-21 19:15:19 +01:00
|
|
|
$rateLimitResponse = $this->checkRateLimit();
|
|
|
|
|
if ($rateLimitResponse !== null) {
|
|
|
|
|
return $rateLimitResponse;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
$licenseKey = $request->get_param('license_key');
|
|
|
|
|
$domain = $request->get_param('domain');
|
|
|
|
|
|
|
|
|
|
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
|
|
|
|
|
|
|
|
|
if (!$license) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'license_not_found',
|
|
|
|
|
'message' => __('License key not found.', 'wc-licensed-product'),
|
|
|
|
|
], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$license->isValid()) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'license_invalid',
|
|
|
|
|
'message' => __('This license is not valid.', 'wc-licensed-product'),
|
|
|
|
|
], 403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
|
|
|
|
|
|
|
|
|
// Check if already activated on this domain
|
|
|
|
|
if ($license->getDomain() === $normalizedDomain) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if can activate on another domain
|
|
|
|
|
if (!$license->canActivate()) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'max_activations_reached',
|
|
|
|
|
'message' => __('Maximum number of activations reached.', 'wc-licensed-product'),
|
|
|
|
|
], 403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update domain (in this simple implementation, we replace the domain)
|
|
|
|
|
$success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);
|
|
|
|
|
|
|
|
|
|
if (!$success) {
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'error' => 'activation_failed',
|
|
|
|
|
'message' => __('Failed to activate license.', 'wc-licensed-product'),
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new WP_REST_Response([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __('License activated successfully.', 'wc-licensed-product'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|