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:
2026-01-26 17:00:52 +01:00
parent bee9854c18
commit 5d5bb7e595
4 changed files with 111 additions and 14 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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__));