You've already forked wc-licensed-product
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 086755cb11 | |||
| 0b58de193e | |||
| ae49b262fa | |||
| 5d5bb7e595 | |||
| bee9854c18 |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.5] - 2026-01-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL:** Response signing key derivation now uses native `hash_hkdf()` for RFC 5869 compliance
|
||||||
|
- Key derivation now matches client library (`SecureLicenseClient`) exactly
|
||||||
|
- Added missing domain validation to `/activate` endpoint (1-255 characters)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `ResponseSigner::deriveCustomerSecret()` now uses `hash_hkdf('sha256', $serverSecret, 32, $licenseKey)`
|
||||||
|
- Previous custom HKDF-like implementation was incompatible with client library
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Signatures generated by server now verify correctly with `magdev/wc-licensed-product-client`
|
||||||
|
- All three API endpoints now have consistent parameter validation
|
||||||
|
|
||||||
|
## [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
|
## [0.5.3] - 2026-01-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
59
CLAUDE.md
59
CLAUDE.md
@@ -1378,3 +1378,62 @@ Major feature release adding support for WooCommerce variable products. Customer
|
|||||||
- Order meta `_licensed_product_domains` now includes optional `variation_id` field
|
- Order meta `_licensed_product_domains` now includes optional `variation_id` field
|
||||||
- License generation uses variation settings when `variation_id` is present in order item
|
- License generation uses variation settings when `variation_id` is present in order item
|
||||||
- Backward compatible: existing simple licensed products continue to work unchanged
|
- 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
|
||||||
|
|
||||||
|
### 2026-01-26 - Version 0.5.5 - Critical Signature Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Critical bug fix for response signing. The key derivation algorithm was incompatible with the client library, causing signature verification failures.
|
||||||
|
|
||||||
|
**Critical Fix:**
|
||||||
|
|
||||||
|
- Key derivation now uses PHP's native `hash_hkdf()` function per RFC 5869
|
||||||
|
- Previous custom implementation produced different keys than the client library
|
||||||
|
- Signatures now verify correctly with `magdev/wc-licensed-product-client`
|
||||||
|
|
||||||
|
**Additional Fix:**
|
||||||
|
|
||||||
|
- Added missing domain validation to `/activate` endpoint (1-255 characters)
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Api/ResponseSigner.php` - Fixed key derivation to use `hash_hkdf()`
|
||||||
|
- `src/Api/RestApiController.php` - Added domain validation to `/activate` endpoint
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Old implementation: `hash_hmac('sha256', $prk . "\x01", $serverSecret)` - custom HKDF-like
|
||||||
|
- New implementation: `bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey))` - RFC 5869
|
||||||
|
- Parameters match client's `ResponseSignature::deriveKey()` exactly:
|
||||||
|
- IKM (input keying material): server_secret
|
||||||
|
- Length: 32 bytes (256 bits)
|
||||||
|
- Info: license_key (context-specific info)
|
||||||
|
- **Breaking change for existing signatures** - customer secrets will change after upgrade
|
||||||
|
|||||||
4
composer.lock
generated
4
composer.lock
generated
@@ -12,7 +12,7 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
||||||
"reference": "64d215cb265a64ff318cfbb954dd128b0076dc1d"
|
"reference": "5e4b5a970f75d0163c5496581d963a24ade4f276"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
||||||
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
||||||
},
|
},
|
||||||
"time": "2026-01-24T13:32:11+00:00"
|
"time": "2026-01-26T15:54:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
|
|||||||
Binary file not shown.
@@ -4,8 +4,8 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WC Licensed Product 0.5.0\n"
|
"Project-Id-Version: WC Licensed Product 0.5.0\n"
|
||||||
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-01-26 16:08+0100\n"
|
"POT-Creation-Date: 2026-01-26 17:06+0100\n"
|
||||||
"PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
|
"PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
|
||||||
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
|
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
|
||||||
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
|
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
|
||||||
@@ -1282,32 +1282,32 @@ msgstr "Lizenz erfolgreich überprüft!"
|
|||||||
msgid "License validation failed."
|
msgid "License validation failed."
|
||||||
msgstr "Lizenzvalidierung fehlgeschlagen."
|
msgstr "Lizenzvalidierung fehlgeschlagen."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:84
|
#: src/Api/RestApiController.php:106
|
||||||
msgid "Too many requests. Please try again later."
|
msgid "Too many requests. Please try again later."
|
||||||
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
#: src/Api/RestApiController.php:400 src/Api/RestApiController.php:433
|
||||||
#: src/License/LicenseManager.php:475
|
#: src/License/LicenseManager.php:475
|
||||||
msgid "License key not found."
|
msgid "License key not found."
|
||||||
msgstr "Lizenzschlüssel nicht gefunden."
|
msgstr "Lizenzschlüssel nicht gefunden."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:386
|
#: src/Api/RestApiController.php:441
|
||||||
msgid "This license is not valid."
|
msgid "This license is not valid."
|
||||||
msgstr "Diese Lizenz ist ungültig."
|
msgstr "Diese Lizenz ist ungültig."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:396
|
#: src/Api/RestApiController.php:451
|
||||||
msgid "License is already activated for this domain."
|
msgid "License is already activated for this domain."
|
||||||
msgstr "Die Lizenz ist bereits für diese Domain aktiviert."
|
msgstr "Die Lizenz ist bereits für diese Domain aktiviert."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:405
|
#: src/Api/RestApiController.php:460
|
||||||
msgid "Maximum number of activations reached."
|
msgid "Maximum number of activations reached."
|
||||||
msgstr "Maximale Anzahl der Aktivierungen erreicht."
|
msgstr "Maximale Anzahl der Aktivierungen erreicht."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:416
|
#: src/Api/RestApiController.php:471
|
||||||
msgid "Failed to activate license."
|
msgid "Failed to activate license."
|
||||||
msgstr "Lizenz konnte nicht aktiviert werden."
|
msgstr "Lizenz konnte nicht aktiviert werden."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:422
|
#: src/Api/RestApiController.php:477
|
||||||
msgid "License activated successfully."
|
msgid "License activated successfully."
|
||||||
msgstr "Lizenz erfolgreich aktiviert."
|
msgstr "Lizenz erfolgreich aktiviert."
|
||||||
|
|
||||||
@@ -1432,6 +1432,15 @@ msgstr "Diese Lizenz ist inaktiv."
|
|||||||
msgid "This license is not valid for this domain."
|
msgid "This license is not valid for this domain."
|
||||||
msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
||||||
|
|
||||||
|
#: src/Product/VersionManager.php:166
|
||||||
|
msgid "Attachment file not found."
|
||||||
|
msgstr "Anhangs-Datei nicht gefunden."
|
||||||
|
|
||||||
|
#: src/Product/VersionManager.php:177
|
||||||
|
#, php-format
|
||||||
|
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
||||||
|
msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:72
|
#: src/Product/LicensedProductType.php:72
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr "Lizensiertes Produkt"
|
msgstr "Lizensiertes Produkt"
|
||||||
@@ -1523,15 +1532,6 @@ msgstr "Leer lassen für übergeordneten Standard. 0 = Lebenslang."
|
|||||||
msgid "Leave empty for parent default."
|
msgid "Leave empty for parent default."
|
||||||
msgstr "Leer lassen für übergeordneten Standard."
|
msgstr "Leer lassen für übergeordneten Standard."
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:166
|
|
||||||
msgid "Attachment file not found."
|
|
||||||
msgstr "Anhangs-Datei nicht gefunden."
|
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:177
|
|
||||||
#, php-format
|
|
||||||
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
|
||||||
msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s"
|
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:143
|
#: src/Product/LicensedProductVariation.php:143
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr "Monatlich"
|
msgstr "Monatlich"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
# SOME DESCRIPTIVE TITLE.
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
# This file is distributed under the same license as the WC Licensed Product package.
|
# This file is distributed under the same license as the wc-licensed-product package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WC Licensed Product 0.5.3\n"
|
"Project-Id-Version: wc-licensed-product 0.5.5\n"
|
||||||
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-01-26 16:08+0100\n"
|
"POT-Creation-Date: 2026-01-26 17:06+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -1238,32 +1238,32 @@ msgstr ""
|
|||||||
msgid "License validation failed."
|
msgid "License validation failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:84
|
#: src/Api/RestApiController.php:106
|
||||||
msgid "Too many requests. Please try again later."
|
msgid "Too many requests. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
#: src/Api/RestApiController.php:400 src/Api/RestApiController.php:433
|
||||||
#: src/License/LicenseManager.php:475
|
#: src/License/LicenseManager.php:475
|
||||||
msgid "License key not found."
|
msgid "License key not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:386
|
#: src/Api/RestApiController.php:441
|
||||||
msgid "This license is not valid."
|
msgid "This license is not valid."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:396
|
#: src/Api/RestApiController.php:451
|
||||||
msgid "License is already activated for this domain."
|
msgid "License is already activated for this domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:405
|
#: src/Api/RestApiController.php:460
|
||||||
msgid "Maximum number of activations reached."
|
msgid "Maximum number of activations reached."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:416
|
#: src/Api/RestApiController.php:471
|
||||||
msgid "Failed to activate license."
|
msgid "Failed to activate license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:422
|
#: src/Api/RestApiController.php:477
|
||||||
msgid "License activated successfully."
|
msgid "License activated successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1383,6 +1383,15 @@ msgstr ""
|
|||||||
msgid "This license is not valid for this domain."
|
msgid "This license is not valid for this domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/VersionManager.php:166
|
||||||
|
msgid "Attachment file not found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/VersionManager.php:177
|
||||||
|
#, php-format
|
||||||
|
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:72
|
#: src/Product/LicensedProductType.php:72
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1472,15 +1481,6 @@ msgstr ""
|
|||||||
msgid "Leave empty for parent default."
|
msgid "Leave empty for parent default."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:166
|
|
||||||
msgid "Attachment file not found."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:177
|
|
||||||
#, php-format
|
|
||||||
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:143
|
#: src/Product/LicensedProductVariation.php:143
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
BIN
releases/wc-licensed-product-0.5.3.zip
Normal file
BIN
releases/wc-licensed-product-0.5.3.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.3.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.3.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bbd0fa8888c6990a4ba00ccfb8b2189ee6ac529a34cc11a5d8d8d28518b1f6dd wc-licensed-product-0.5.3.zip
|
||||||
@@ -157,16 +157,23 @@ final class ResponseSigner
|
|||||||
* to verify signed API responses. Each customer gets their own secret
|
* to verify signed API responses. Each customer gets their own secret
|
||||||
* derived from their license key.
|
* derived from their license key.
|
||||||
*
|
*
|
||||||
|
* Uses RFC 5869 HKDF via PHP's native hash_hkdf() function.
|
||||||
|
* Parameters match the client library (SecureLicenseClient):
|
||||||
|
* - IKM (input keying material): server_secret
|
||||||
|
* - Length: 32 bytes (256 bits for SHA-256)
|
||||||
|
* - Info: license_key (context-specific info)
|
||||||
|
*
|
||||||
* @param string $licenseKey The customer's license key
|
* @param string $licenseKey The customer's license key
|
||||||
* @param string $serverSecret The server's master secret
|
* @param string $serverSecret The server's master secret
|
||||||
* @return string The derived secret (64 hex characters)
|
* @return string The derived secret (64 hex characters)
|
||||||
*/
|
*/
|
||||||
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
|
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
|
||||||
{
|
{
|
||||||
// HKDF-like key derivation
|
// RFC 5869 HKDF using PHP's native implementation
|
||||||
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
|
// Must match client's ResponseSignature::deriveKey() exactly
|
||||||
|
$binaryKey = hash_hkdf('sha256', $serverSecret, 32, $licenseKey);
|
||||||
|
|
||||||
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
|
return bin2hex($binaryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,14 +22,34 @@ final class RestApiController
|
|||||||
private const NAMESPACE = 'wc-licensed-product/v1';
|
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;
|
private LicenseManager $licenseManager;
|
||||||
|
|
||||||
@@ -56,12 +76,14 @@ final class RestApiController
|
|||||||
{
|
{
|
||||||
$ip = $this->getClientIp();
|
$ip = $this->getClientIp();
|
||||||
$transientKey = 'wclp_rate_' . md5($ip);
|
$transientKey = 'wclp_rate_' . md5($ip);
|
||||||
|
$rateLimit = $this->getRateLimit();
|
||||||
|
$rateWindow = $this->getRateWindow();
|
||||||
|
|
||||||
$data = get_transient($transientKey);
|
$data = get_transient($transientKey);
|
||||||
|
|
||||||
if ($data === false) {
|
if ($data === false) {
|
||||||
// First request, start counting
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +91,15 @@ final class RestApiController
|
|||||||
$start = (int) ($data['start'] ?? time());
|
$start = (int) ($data['start'] ?? time());
|
||||||
|
|
||||||
// Check if window has expired
|
// Check if window has expired
|
||||||
if (time() - $start >= self::RATE_LIMIT_WINDOW) {
|
if (time() - $start >= $rateWindow) {
|
||||||
// Reset counter
|
// Reset counter
|
||||||
set_transient($transientKey, ['count' => 1, 'start' => time()], self::RATE_LIMIT_WINDOW);
|
set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if limit exceeded
|
// Check if limit exceeded
|
||||||
if ($count >= self::RATE_LIMIT_REQUESTS) {
|
if ($count >= $rateLimit) {
|
||||||
$retryAfter = self::RATE_LIMIT_WINDOW - (time() - $start);
|
$retryAfter = $rateWindow - (time() - $start);
|
||||||
$response = new WP_REST_Response([
|
$response = new WP_REST_Response([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'rate_limit_exceeded',
|
'error' => 'rate_limit_exceeded',
|
||||||
@@ -89,7 +111,7 @@ final class RestApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Increment counter
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +279,8 @@ final class RestApiController
|
|||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
'validate_callback' => function ($value): bool {
|
'validate_callback' => function ($value): bool {
|
||||||
return !empty($value) && strlen($value) <= 64;
|
$len = strlen($value);
|
||||||
|
return !empty($value) && $len >= 8 && $len <= 64;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'domain' => [
|
'domain' => [
|
||||||
@@ -281,6 +304,10 @@ final class RestApiController
|
|||||||
'required' => true,
|
'required' => true,
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ($value): bool {
|
||||||
|
$len = strlen($value);
|
||||||
|
return !empty($value) && $len >= 8 && $len <= 64;
|
||||||
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -295,11 +322,18 @@ final class RestApiController
|
|||||||
'required' => true,
|
'required' => true,
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ($value): bool {
|
||||||
|
$len = strlen($value);
|
||||||
|
return !empty($value) && $len >= 8 && $len <= 64;
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'domain' => [
|
'domain' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ($value): bool {
|
||||||
|
return !empty($value) && strlen($value) <= 255;
|
||||||
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -320,11 +354,32 @@ final class RestApiController
|
|||||||
|
|
||||||
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||||
|
|
||||||
$statusCode = $result['valid'] ? 200 : 403;
|
$statusCode = $this->getStatusCodeForResult($result);
|
||||||
|
|
||||||
return new WP_REST_Response($result, $statusCode);
|
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
|
* Check license status endpoint
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Licensed Product
|
* Plugin Name: WooCommerce Licensed Product
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-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.
|
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||||
* Version: 0.5.3
|
* Version: 0.5.5
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('WC_LICENSED_PRODUCT_VERSION', '0.5.3');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.5.5');
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user