You've already forked wc-licensed-product
Align REST API with client documentation (v0.5.4)
Fixed HTTP status codes for API responses: - /validate now returns 404 for license_not_found (was 403) - Added status code mapping: 404 not found, 500 server errors, 403 others Added configurable rate limiting: - WC_LICENSE_RATE_LIMIT constant for requests per window - WC_LICENSE_RATE_WINDOW constant for window duration in seconds Fixed license_key validation: - Now enforces minimum 8 characters across all endpoints Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.4] - 2026-01-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- REST API `/validate` endpoint now returns HTTP 404 for `license_not_found` error (was 403)
|
||||
- License key validation now enforces minimum 8 characters per API documentation
|
||||
|
||||
### Added
|
||||
|
||||
- Configurable rate limiting via `WC_LICENSE_RATE_LIMIT` and `WC_LICENSE_RATE_WINDOW` constants
|
||||
- Rate limit now defaults to 30 requests per 60 second window (configurable)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved HTTP status code mapping: 404 for not found, 500 for server errors, 403 for all other errors
|
||||
- Rate limiting implementation now uses configurable constants instead of hardcoded values
|
||||
|
||||
## [0.5.3] - 2026-01-26
|
||||
|
||||
### Added
|
||||
|
||||
28
CLAUDE.md
28
CLAUDE.md
@@ -1378,3 +1378,31 @@ Major feature release adding support for WooCommerce variable products. Customer
|
||||
- Order meta `_licensed_product_domains` now includes optional `variation_id` field
|
||||
- License generation uses variation settings when `variation_id` is present in order item
|
||||
- Backward compatible: existing simple licensed products continue to work unchanged
|
||||
|
||||
### 2026-01-26 - Version 0.5.4 - API Compliance
|
||||
|
||||
**Overview:**
|
||||
|
||||
Bug fix release aligning server implementation with client documentation at `magdev/wc-licensed-product-client`.
|
||||
|
||||
**Fixed:**
|
||||
|
||||
- `/validate` endpoint now returns HTTP 404 for `license_not_found` error (was returning 403)
|
||||
- License key validation now enforces minimum 8 characters across all API endpoints
|
||||
|
||||
**Implemented:**
|
||||
|
||||
- Configurable rate limiting via `WC_LICENSE_RATE_LIMIT` constant (default: 30 requests)
|
||||
- Configurable rate window via `WC_LICENSE_RATE_WINDOW` constant (default: 60 seconds)
|
||||
- HTTP status code mapping: 404 for not found, 500 for server errors, 403 for all other errors
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Api/RestApiController.php` - Added configurable rate limiting, fixed HTTP status codes, added license_key validation
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Rate limiting now uses `getRateLimit()` and `getRateWindow()` methods instead of constants
|
||||
- New `getStatusCodeForResult()` method maps error codes to HTTP status codes
|
||||
- License key validation callback added to all three endpoints (validate, status, activate)
|
||||
- Uses PHP 8 match expression for status code mapping
|
||||
|
||||
@@ -22,14 +22,34 @@ final class RestApiController
|
||||
private const NAMESPACE = 'wc-licensed-product/v1';
|
||||
|
||||
/**
|
||||
* Rate limit: requests per minute per IP
|
||||
* Default rate limit: requests per window per IP
|
||||
*/
|
||||
private const RATE_LIMIT_REQUESTS = 30;
|
||||
private const DEFAULT_RATE_LIMIT = 30;
|
||||
|
||||
/**
|
||||
* Rate limit window in seconds
|
||||
* Default rate limit window in seconds
|
||||
*/
|
||||
private const RATE_LIMIT_WINDOW = 60;
|
||||
private const DEFAULT_RATE_WINDOW = 60;
|
||||
|
||||
/**
|
||||
* Get the configured rate limit (requests per window)
|
||||
*/
|
||||
private function getRateLimit(): int
|
||||
{
|
||||
return defined('WC_LICENSE_RATE_LIMIT')
|
||||
? (int) WC_LICENSE_RATE_LIMIT
|
||||
: self::DEFAULT_RATE_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured rate limit window in seconds
|
||||
*/
|
||||
private function getRateWindow(): int
|
||||
{
|
||||
return defined('WC_LICENSE_RATE_WINDOW')
|
||||
? (int) WC_LICENSE_RATE_WINDOW
|
||||
: self::DEFAULT_RATE_WINDOW;
|
||||
}
|
||||
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
@@ -56,12 +76,14 @@ final class RestApiController
|
||||
{
|
||||
$ip = $this->getClientIp();
|
||||
$transientKey = 'wclp_rate_' . md5($ip);
|
||||
$rateLimit = $this->getRateLimit();
|
||||
$rateWindow = $this->getRateWindow();
|
||||
|
||||
$data = get_transient($transientKey);
|
||||
|
||||
if ($data === false) {
|
||||
// First request, start counting
|
||||
set_transient($transientKey, ['count' => 1, 'start' => time()], self::RATE_LIMIT_WINDOW);
|
||||
set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -69,15 +91,15 @@ final class RestApiController
|
||||
$start = (int) ($data['start'] ?? time());
|
||||
|
||||
// Check if window has expired
|
||||
if (time() - $start >= self::RATE_LIMIT_WINDOW) {
|
||||
if (time() - $start >= $rateWindow) {
|
||||
// Reset counter
|
||||
set_transient($transientKey, ['count' => 1, 'start' => time()], self::RATE_LIMIT_WINDOW);
|
||||
set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if limit exceeded
|
||||
if ($count >= self::RATE_LIMIT_REQUESTS) {
|
||||
$retryAfter = self::RATE_LIMIT_WINDOW - (time() - $start);
|
||||
if ($count >= $rateLimit) {
|
||||
$retryAfter = $rateWindow - (time() - $start);
|
||||
$response = new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'rate_limit_exceeded',
|
||||
@@ -89,7 +111,7 @@ final class RestApiController
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
set_transient($transientKey, ['count' => $count + 1, 'start' => $start], self::RATE_LIMIT_WINDOW);
|
||||
set_transient($transientKey, ['count' => $count + 1, 'start' => $start], $rateWindow);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -257,7 +279,8 @@ final class RestApiController
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => function ($value): bool {
|
||||
return !empty($value) && strlen($value) <= 64;
|
||||
$len = strlen($value);
|
||||
return !empty($value) && $len >= 8 && $len <= 64;
|
||||
},
|
||||
],
|
||||
'domain' => [
|
||||
@@ -281,6 +304,10 @@ final class RestApiController
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => function ($value): bool {
|
||||
$len = strlen($value);
|
||||
return !empty($value) && $len >= 8 && $len <= 64;
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
@@ -295,6 +322,10 @@ final class RestApiController
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => function ($value): bool {
|
||||
$len = strlen($value);
|
||||
return !empty($value) && $len >= 8 && $len <= 64;
|
||||
},
|
||||
],
|
||||
'domain' => [
|
||||
'required' => true,
|
||||
@@ -320,11 +351,32 @@ final class RestApiController
|
||||
|
||||
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||
|
||||
$statusCode = $result['valid'] ? 200 : 403;
|
||||
$statusCode = $this->getStatusCodeForResult($result);
|
||||
|
||||
return new WP_REST_Response($result, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP status code based on validation result
|
||||
*
|
||||
* @param array $result The validation result
|
||||
* @return int HTTP status code
|
||||
*/
|
||||
private function getStatusCodeForResult(array $result): int
|
||||
{
|
||||
if ($result['valid']) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
$error = $result['error'] ?? '';
|
||||
|
||||
return match ($error) {
|
||||
'license_not_found' => 404,
|
||||
'activation_failed' => 500,
|
||||
default => 403,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check license status endpoint
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WooCommerce Licensed Product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||
* Version: 0.5.3
|
||||
* Version: 0.5.4
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.5.3');
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.5.4');
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user