You've already forked wc-licensed-product-client
Add update-check endpoint support (v0.2.1)
Implement /update-check endpoint aligned with remote OpenAPI spec: - Add checkForUpdates() method to LicenseClientInterface - Add UpdateInfo DTO for update check responses - Add ProductNotFoundException for product_not_found error - Update local openapi.json to v0.4.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.1] - 2026-01-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `checkForUpdates()` method for checking plugin updates
|
||||||
|
- `UpdateInfo` DTO for update check responses
|
||||||
|
- `ProductNotFoundException` for `product_not_found` error handling
|
||||||
|
- `/update-check` endpoint support aligned with remote OpenAPI spec (v0.4.0)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated local `openapi.json` to match remote specification (now v0.4.0)
|
||||||
|
- Added "Plugin Updates" tag to OpenAPI specification
|
||||||
|
|
||||||
## [0.2.0] - 2026-01-26
|
## [0.2.0] - 2026-01-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
28
CLAUDE.md
28
CLAUDE.md
@@ -29,10 +29,6 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
No known bugs at the moment
|
No known bugs at the moment
|
||||||
|
|
||||||
### Version 0.2.1
|
|
||||||
|
|
||||||
No pending tasks at the moment.
|
|
||||||
|
|
||||||
### Version 0.3.0
|
### Version 0.3.0
|
||||||
|
|
||||||
No pending tasks at the moment.
|
No pending tasks at the moment.
|
||||||
@@ -237,3 +233,27 @@ When editing CLAUDE.md or other markdown files, follow these rules to avoid lint
|
|||||||
- Version tagging with `git tag -a` creates annotated tags with messages
|
- Version tagging with `git tag -a` creates annotated tags with messages
|
||||||
- CHANGELOG.md follows Keep a Changelog format (MD024 duplicate headings are expected)
|
- CHANGELOG.md follows Keep a Changelog format (MD024 duplicate headings are expected)
|
||||||
- Roadmap in CLAUDE.md should be updated after each release to reflect next versions
|
- Roadmap in CLAUDE.md should be updated after each release to reflect next versions
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.2.1 (Update Check Endpoint)
|
||||||
|
|
||||||
|
**Completed:**
|
||||||
|
|
||||||
|
- Compared local `openapi.json` against remote server specification
|
||||||
|
- Discovered new `/update-check` endpoint in remote spec
|
||||||
|
- Added `UpdateInfo` DTO for update check responses
|
||||||
|
- Added `ProductNotFoundException` for `product_not_found` error code
|
||||||
|
- Implemented `checkForUpdates()` method in `LicenseClientInterface`
|
||||||
|
- Implemented `checkForUpdates()` in both `LicenseClient` and `SecureLicenseClient`
|
||||||
|
- Updated local `openapi.json` with `/update-check` endpoint (now v0.4.0)
|
||||||
|
- Updated `LicenseException::fromApiResponse()` to handle `product_not_found`
|
||||||
|
- Added encoded constant `ENCODED_UPDATE_CHECK` to `SecureLicenseClient`
|
||||||
|
- Updated documentation: README.md and docs/client-implementation.md
|
||||||
|
- All 66 tests pass
|
||||||
|
|
||||||
|
**Learnings:**
|
||||||
|
|
||||||
|
- Remote OpenAPI spec at server repo may have newer endpoints than local copy
|
||||||
|
- Update check endpoint returns WordPress-compatible plugin info (tested, requires, requires_php)
|
||||||
|
- `UpdateInfo` DTO supports both `download_url` and `package` fields (WordPress compatibility)
|
||||||
|
- Package hash format uses `sha256:hexstring` prefix for integrity verification
|
||||||
|
- StringEncoder can generate encoded constants for obfuscated endpoint names
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ composer require magdev/wc-licensed-product-client
|
|||||||
- License validation against domains
|
- License validation against domains
|
||||||
- License activation on domains
|
- License activation on domains
|
||||||
- License status checking
|
- License status checking
|
||||||
|
- Plugin update checking
|
||||||
- Comprehensive exception handling
|
- Comprehensive exception handling
|
||||||
- Code integrity verification
|
- Code integrity verification
|
||||||
- Built on Symfony HttpClient
|
- Built on Symfony HttpClient
|
||||||
@@ -52,6 +53,12 @@ echo "Status: " . $status->status->value;
|
|||||||
// Activate a license
|
// Activate a license
|
||||||
$result = $client->activate('ABCD-1234-EFGH-5678', 'example.com');
|
$result = $client->activate('ABCD-1234-EFGH-5678', 'example.com');
|
||||||
echo "Activated: " . ($result->success ? 'Yes' : 'No');
|
echo "Activated: " . ($result->success ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
$updateInfo = $client->checkForUpdates('ABCD-1234-EFGH-5678', 'example.com', 'my-plugin', '1.0.0');
|
||||||
|
if ($updateInfo->updateAvailable) {
|
||||||
|
echo "New version available: " . $updateInfo->version;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Logging
|
### With Logging
|
||||||
@@ -160,6 +167,7 @@ This client interacts with the following WooCommerce Licensed Product API endpoi
|
|||||||
- **POST /validate** - Validate a license key for a specific domain
|
- **POST /validate** - Validate a license key for a specific domain
|
||||||
- **POST /status** - Get detailed license status information
|
- **POST /status** - Get detailed license status information
|
||||||
- **POST /activate** - Activate a license on a domain
|
- **POST /activate** - Activate a license on a domain
|
||||||
|
- **POST /update-check** - Check for plugin updates
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ composer require magdev/wc-licensed-product-client
|
|||||||
│ + validate() │
|
│ + validate() │
|
||||||
│ + status() │
|
│ + status() │
|
||||||
│ + activate() │
|
│ + activate() │
|
||||||
|
│ + checkForUpdates() │
|
||||||
└─────────────┬───────────────┘
|
└─────────────┬───────────────┘
|
||||||
│ implements
|
│ implements
|
||||||
├───────────────────────┐
|
├───────────────────────┐
|
||||||
@@ -57,6 +58,7 @@ composer require magdev/wc-licensed-product-client
|
|||||||
│ LicenseInfo │
|
│ LicenseInfo │
|
||||||
│ LicenseStatus │
|
│ LicenseStatus │
|
||||||
│ ActivationResult │
|
│ ActivationResult │
|
||||||
|
│ UpdateInfo │
|
||||||
│ LicenseState (enum) │
|
│ LicenseState (enum) │
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -71,7 +73,8 @@ src/
|
|||||||
├── Dto/
|
├── Dto/
|
||||||
│ ├── LicenseInfo.php # Validation response DTO
|
│ ├── LicenseInfo.php # Validation response DTO
|
||||||
│ ├── LicenseStatus.php # Status response DTO + LicenseState enum
|
│ ├── LicenseStatus.php # Status response DTO + LicenseState enum
|
||||||
│ └── ActivationResult.php # Activation response DTO
|
│ ├── ActivationResult.php # Activation response DTO
|
||||||
|
│ └── UpdateInfo.php # Update check response DTO
|
||||||
├── Exception/
|
├── Exception/
|
||||||
│ ├── LicenseException.php # Base exception
|
│ ├── LicenseException.php # Base exception
|
||||||
│ ├── LicenseNotFoundException.php
|
│ ├── LicenseNotFoundException.php
|
||||||
@@ -82,6 +85,7 @@ src/
|
|||||||
│ ├── DomainMismatchException.php
|
│ ├── DomainMismatchException.php
|
||||||
│ ├── MaxActivationsReachedException.php
|
│ ├── MaxActivationsReachedException.php
|
||||||
│ ├── ActivationFailedException.php
|
│ ├── ActivationFailedException.php
|
||||||
|
│ ├── ProductNotFoundException.php
|
||||||
│ └── RateLimitExceededException.php
|
│ └── RateLimitExceededException.php
|
||||||
└── Security/
|
└── Security/
|
||||||
├── ResponseSignature.php # HMAC signature verification
|
├── ResponseSignature.php # HMAC signature verification
|
||||||
@@ -120,6 +124,18 @@ interface LicenseClientInterface
|
|||||||
* @throws LicenseException When activation fails
|
* @throws LicenseException When activation fails
|
||||||
*/
|
*/
|
||||||
public function activate(string $licenseKey, string $domain): ActivationResult;
|
public function activate(string $licenseKey, string $domain): ActivationResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for available plugin updates.
|
||||||
|
*
|
||||||
|
* @throws LicenseException When update check fails
|
||||||
|
*/
|
||||||
|
public function checkForUpdates(
|
||||||
|
string $licenseKey,
|
||||||
|
string $domain,
|
||||||
|
?string $pluginSlug = null,
|
||||||
|
?string $currentVersion = null,
|
||||||
|
): UpdateInfo;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -306,6 +322,57 @@ if ($result->success) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### UpdateInfo
|
||||||
|
|
||||||
|
Returned by `checkForUpdates()`. Contains plugin update information.
|
||||||
|
|
||||||
|
```php
|
||||||
|
final readonly class UpdateInfo
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public bool $updateAvailable, // Whether an update is available
|
||||||
|
public ?string $version, // Latest available version
|
||||||
|
public ?string $slug, // Plugin slug for WordPress
|
||||||
|
public ?string $plugin, // Plugin basename (slug/slug.php)
|
||||||
|
public ?string $downloadUrl, // Secure download URL
|
||||||
|
public ?\DateTimeImmutable $lastUpdated,// Date of the latest release
|
||||||
|
public ?string $tested, // Highest WordPress version tested
|
||||||
|
public ?string $requires, // Minimum WordPress version
|
||||||
|
public ?string $requiresPhp, // Minimum PHP version
|
||||||
|
public ?string $changelog, // Release notes
|
||||||
|
public ?string $packageHash, // SHA256 hash for integrity
|
||||||
|
public ?string $name, // Product name
|
||||||
|
public ?string $homepage, // Product homepage URL
|
||||||
|
public ?array $icons, // Plugin icons for WordPress admin
|
||||||
|
public ?array $sections, // Content sections for plugin info
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): self;
|
||||||
|
|
||||||
|
public function hasValidPackageHash(): bool; // Check package hash format
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$updateInfo = $client->checkForUpdates(
|
||||||
|
'ABCD-1234-EFGH-5678',
|
||||||
|
'example.com',
|
||||||
|
'my-plugin',
|
||||||
|
'1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($updateInfo->updateAvailable) {
|
||||||
|
echo "New version available: " . $updateInfo->version;
|
||||||
|
echo "Download URL: " . $updateInfo->downloadUrl;
|
||||||
|
|
||||||
|
if ($updateInfo->hasValidPackageHash()) {
|
||||||
|
echo "Package hash: " . $updateInfo->packageHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Exception Hierarchy
|
## Exception Hierarchy
|
||||||
|
|
||||||
All exceptions extend `LicenseException` and include an `errorCode` property for programmatic handling.
|
All exceptions extend `LicenseException` and include an `errorCode` property for programmatic handling.
|
||||||
@@ -320,8 +387,9 @@ LicenseException (base)
|
|||||||
├── DomainMismatchException // error: domain_mismatch
|
├── DomainMismatchException // error: domain_mismatch
|
||||||
├── MaxActivationsReachedException // error: max_activations_reached
|
├── MaxActivationsReachedException // error: max_activations_reached
|
||||||
├── ActivationFailedException // error: activation_failed
|
├── ActivationFailedException // error: activation_failed
|
||||||
|
├── ProductNotFoundException // error: product_not_found
|
||||||
├── RateLimitExceededException // error: rate_limit_exceeded (has retryAfter)
|
├── RateLimitExceededException // error: rate_limit_exceeded (has retryAfter)
|
||||||
└── Security\SignatureException // error: signature_invalid
|
├── Security\SignatureException // error: signature_invalid
|
||||||
└── Security\IntegrityException // error: integrity_check_failed
|
└── Security\IntegrityException // error: integrity_check_failed
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -925,6 +993,7 @@ The client communicates with these REST API endpoints:
|
|||||||
| `/wp-json/wc-licensed-product/v1/validate` | POST | Validate license for domain |
|
| `/wp-json/wc-licensed-product/v1/validate` | POST | Validate license for domain |
|
||||||
| `/wp-json/wc-licensed-product/v1/status` | POST | Get license status details |
|
| `/wp-json/wc-licensed-product/v1/status` | POST | Get license status details |
|
||||||
| `/wp-json/wc-licensed-product/v1/activate` | POST | Activate license on domain |
|
| `/wp-json/wc-licensed-product/v1/activate` | POST | Activate license on domain |
|
||||||
|
| `/wp-json/wc-licensed-product/v1/update-check` | POST | Check for plugin updates |
|
||||||
|
|
||||||
### Request Format
|
### Request Format
|
||||||
|
|
||||||
@@ -974,6 +1043,7 @@ All requests use JSON bodies:
|
|||||||
| `domain_mismatch` | `DomainMismatchException` | Domain not authorized |
|
| `domain_mismatch` | `DomainMismatchException` | Domain not authorized |
|
||||||
| `max_activations_reached` | `MaxActivationsReachedException` | Too many activations |
|
| `max_activations_reached` | `MaxActivationsReachedException` | Too many activations |
|
||||||
| `activation_failed` | `ActivationFailedException` | Server error during activation |
|
| `activation_failed` | `ActivationFailedException` | Server error during activation |
|
||||||
|
| `product_not_found` | `ProductNotFoundException` | Licensed product doesn't exist |
|
||||||
| `rate_limit_exceeded` | `RateLimitExceededException` | Too many requests |
|
| `rate_limit_exceeded` | `RateLimitExceededException` | Too many requests |
|
||||||
| `signature_invalid` | `SignatureException` | Response signature invalid |
|
| `signature_invalid` | `SignatureException` | Response signature invalid |
|
||||||
| `integrity_check_failed` | `IntegrityException` | Source files modified |
|
| `integrity_check_failed` | `IntegrityException` | Source files modified |
|
||||||
|
|||||||
110
src/Dto/UpdateInfo.php
Normal file
110
src/Dto/UpdateInfo.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WcLicensedProductClient\Dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents update information returned by the update-check endpoint.
|
||||||
|
*/
|
||||||
|
final readonly class UpdateInfo
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param bool $updateAvailable Whether an update is available
|
||||||
|
* @param string|null $version Latest available version
|
||||||
|
* @param string|null $slug Plugin slug for WordPress
|
||||||
|
* @param string|null $plugin Plugin basename (slug/slug.php)
|
||||||
|
* @param string|null $downloadUrl Secure download URL for the update package
|
||||||
|
* @param \DateTimeImmutable|null $lastUpdated Date of the latest release
|
||||||
|
* @param string|null $tested Highest WordPress version tested with
|
||||||
|
* @param string|null $requires Minimum required WordPress version
|
||||||
|
* @param string|null $requiresPhp Minimum required PHP version
|
||||||
|
* @param string|null $changelog Release notes/changelog for the update
|
||||||
|
* @param string|null $packageHash SHA256 hash of the package for integrity verification
|
||||||
|
* @param string|null $name Product name
|
||||||
|
* @param string|null $homepage Product homepage URL
|
||||||
|
* @param array<string, string>|null $icons Plugin icons for WordPress admin
|
||||||
|
* @param array<string, string>|null $sections Content sections for plugin info modal
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public bool $updateAvailable,
|
||||||
|
public ?string $version = null,
|
||||||
|
public ?string $slug = null,
|
||||||
|
public ?string $plugin = null,
|
||||||
|
public ?string $downloadUrl = null,
|
||||||
|
public ?\DateTimeImmutable $lastUpdated = null,
|
||||||
|
public ?string $tested = null,
|
||||||
|
public ?string $requires = null,
|
||||||
|
public ?string $requiresPhp = null,
|
||||||
|
public ?string $changelog = null,
|
||||||
|
public ?string $packageHash = null,
|
||||||
|
public ?string $name = null,
|
||||||
|
public ?string $homepage = null,
|
||||||
|
public ?array $icons = null,
|
||||||
|
public ?array $sections = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
if (!isset($data['update_available']) || !is_bool($data['update_available'])) {
|
||||||
|
throw new \InvalidArgumentException('Invalid response: missing or invalid update_available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastUpdated = null;
|
||||||
|
if (isset($data['last_updated']) && $data['last_updated'] !== null) {
|
||||||
|
try {
|
||||||
|
$lastUpdated = new \DateTimeImmutable($data['last_updated']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
'Invalid response: invalid date format for last_updated',
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$icons = null;
|
||||||
|
if (isset($data['icons']) && is_array($data['icons'])) {
|
||||||
|
$icons = $data['icons'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections = null;
|
||||||
|
if (isset($data['sections']) && is_array($data['sections'])) {
|
||||||
|
$sections = $data['sections'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
updateAvailable: $data['update_available'],
|
||||||
|
version: isset($data['version']) && is_string($data['version']) ? $data['version'] : null,
|
||||||
|
slug: isset($data['slug']) && is_string($data['slug']) ? $data['slug'] : null,
|
||||||
|
plugin: isset($data['plugin']) && is_string($data['plugin']) ? $data['plugin'] : null,
|
||||||
|
downloadUrl: isset($data['download_url']) && is_string($data['download_url']) ? $data['download_url'] : (
|
||||||
|
isset($data['package']) && is_string($data['package']) ? $data['package'] : null
|
||||||
|
),
|
||||||
|
lastUpdated: $lastUpdated,
|
||||||
|
tested: isset($data['tested']) && is_string($data['tested']) ? $data['tested'] : null,
|
||||||
|
requires: isset($data['requires']) && is_string($data['requires']) ? $data['requires'] : null,
|
||||||
|
requiresPhp: isset($data['requires_php']) && is_string($data['requires_php']) ? $data['requires_php'] : null,
|
||||||
|
changelog: isset($data['changelog']) && is_string($data['changelog']) ? $data['changelog'] : null,
|
||||||
|
packageHash: isset($data['package_hash']) && is_string($data['package_hash']) ? $data['package_hash'] : null,
|
||||||
|
name: isset($data['name']) && is_string($data['name']) ? $data['name'] : null,
|
||||||
|
homepage: isset($data['homepage']) && is_string($data['homepage']) ? $data['homepage'] : null,
|
||||||
|
icons: $icons,
|
||||||
|
sections: $sections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the package hash is valid against the expected format.
|
||||||
|
*/
|
||||||
|
public function hasValidPackageHash(): bool
|
||||||
|
{
|
||||||
|
if ($this->packageHash === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package hash format: "sha256:hexstring"
|
||||||
|
return preg_match('/^sha256:[a-f0-9]{64}$/i', $this->packageHash) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ class LicenseException extends \RuntimeException
|
|||||||
'domain_mismatch' => new DomainMismatchException($message, $errorCode, $httpCode),
|
'domain_mismatch' => new DomainMismatchException($message, $errorCode, $httpCode),
|
||||||
'max_activations_reached' => new MaxActivationsReachedException($message, $errorCode, $httpCode),
|
'max_activations_reached' => new MaxActivationsReachedException($message, $errorCode, $httpCode),
|
||||||
'activation_failed' => new ActivationFailedException($message, $errorCode, $httpCode),
|
'activation_failed' => new ActivationFailedException($message, $errorCode, $httpCode),
|
||||||
|
'product_not_found' => new ProductNotFoundException($message, $errorCode, $httpCode),
|
||||||
'rate_limit_exceeded' => new RateLimitExceededException(
|
'rate_limit_exceeded' => new RateLimitExceededException(
|
||||||
$message,
|
$message,
|
||||||
$errorCode,
|
$errorCode,
|
||||||
|
|||||||
9
src/Exception/ProductNotFoundException.php
Normal file
9
src/Exception/ProductNotFoundException.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Magdev\WcLicensedProductClient\Exception;
|
||||||
|
|
||||||
|
final class ProductNotFoundException extends LicenseException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ namespace Magdev\WcLicensedProductClient;
|
|||||||
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -144,6 +145,57 @@ final class LicenseClient implements LicenseClientInterface
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function checkForUpdates(
|
||||||
|
string $licenseKey,
|
||||||
|
string $domain,
|
||||||
|
?string $pluginSlug = null,
|
||||||
|
?string $currentVersion = null,
|
||||||
|
): UpdateInfo {
|
||||||
|
$cacheKey = $this->buildCacheKey('update-check', $licenseKey, $domain);
|
||||||
|
|
||||||
|
if ($this->cache !== null) {
|
||||||
|
$item = $this->cache->getItem($cacheKey);
|
||||||
|
if ($item->isHit()) {
|
||||||
|
$this->logger->debug('Update check cache hit', ['domain' => $domain]);
|
||||||
|
return $item->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Checking for updates', ['domain' => $domain]);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'license_key' => $licenseKey,
|
||||||
|
'domain' => $domain,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($pluginSlug !== null) {
|
||||||
|
$payload['plugin_slug'] = $pluginSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentVersion !== null) {
|
||||||
|
$payload['current_version'] = $currentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->request('update-check', $payload);
|
||||||
|
|
||||||
|
$result = UpdateInfo::fromArray($response);
|
||||||
|
|
||||||
|
if ($this->cache !== null) {
|
||||||
|
$item = $this->cache->getItem($cacheKey);
|
||||||
|
$item->set($result);
|
||||||
|
$item->expiresAfter($this->cacheTtl);
|
||||||
|
$this->cache->save($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Update check completed', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'update_available' => $result->updateAvailable,
|
||||||
|
'version' => $result->version,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LicenseException
|
* @throws LicenseException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Magdev\WcLicensedProductClient;
|
|||||||
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||||
|
|
||||||
interface LicenseClientInterface
|
interface LicenseClientInterface
|
||||||
@@ -31,4 +32,21 @@ interface LicenseClientInterface
|
|||||||
* @throws LicenseException When activation fails
|
* @throws LicenseException When activation fails
|
||||||
*/
|
*/
|
||||||
public function activate(string $licenseKey, string $domain): ActivationResult;
|
public function activate(string $licenseKey, string $domain): ActivationResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for available plugin updates.
|
||||||
|
*
|
||||||
|
* @param string $licenseKey The license key
|
||||||
|
* @param string $domain The domain the plugin is installed on
|
||||||
|
* @param string|null $pluginSlug Optional plugin slug for identification
|
||||||
|
* @param string|null $currentVersion Currently installed version for comparison
|
||||||
|
*
|
||||||
|
* @throws LicenseException When update check fails
|
||||||
|
*/
|
||||||
|
public function checkForUpdates(
|
||||||
|
string $licenseKey,
|
||||||
|
string $domain,
|
||||||
|
?string $pluginSlug = null,
|
||||||
|
?string $currentVersion = null,
|
||||||
|
): UpdateInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Magdev\WcLicensedProductClient;
|
|||||||
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
|
||||||
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
|
||||||
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||||||
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||||
use Magdev\WcLicensedProductClient\Security\IntegrityChecker;
|
use Magdev\WcLicensedProductClient\Security\IntegrityChecker;
|
||||||
use Magdev\WcLicensedProductClient\Security\IntegrityException;
|
use Magdev\WcLicensedProductClient\Security\IntegrityException;
|
||||||
@@ -52,6 +53,7 @@ final class SecureLicenseClient implements LicenseClientInterface
|
|||||||
private const ENCODED_VALIDATE = 'Egs4Oz4HMgE='; // validate
|
private const ENCODED_VALIDATE = 'Egs4Oz4HMgE='; // validate
|
||||||
private const ENCODED_STATUS = 'NwgqKAcZ'; // status
|
private const ENCODED_STATUS = 'NwgqKAcZ'; // status
|
||||||
private const ENCODED_ACTIVATE = 'Jggxfg4MEws='; // activate
|
private const ENCODED_ACTIVATE = 'Jggxfg4MEws='; // activate
|
||||||
|
private const ENCODED_UPDATE_CHECK = 'Z8E5+5jwTPWoY64b'; // update-check
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HttpClientInterface $httpClient,
|
private readonly HttpClientInterface $httpClient,
|
||||||
@@ -173,6 +175,58 @@ final class SecureLicenseClient implements LicenseClientInterface
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function checkForUpdates(
|
||||||
|
string $licenseKey,
|
||||||
|
string $domain,
|
||||||
|
?string $pluginSlug = null,
|
||||||
|
?string $currentVersion = null,
|
||||||
|
): UpdateInfo {
|
||||||
|
$cacheKey = $this->buildCacheKey('update-check', $licenseKey, $domain);
|
||||||
|
|
||||||
|
if ($this->cache !== null) {
|
||||||
|
$item = $this->cache->getItem($cacheKey);
|
||||||
|
if ($item->isHit()) {
|
||||||
|
$this->logger->debug('Update check cache hit', ['domain' => $domain]);
|
||||||
|
return $item->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Checking for updates', ['domain' => $domain]);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'license_key' => $licenseKey,
|
||||||
|
'domain' => $domain,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($pluginSlug !== null) {
|
||||||
|
$payload['plugin_slug'] = $pluginSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentVersion !== null) {
|
||||||
|
$payload['current_version'] = $currentVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = $this->encoder->decode(self::ENCODED_UPDATE_CHECK);
|
||||||
|
$response = $this->secureRequest($endpoint, $payload, $licenseKey);
|
||||||
|
|
||||||
|
$result = UpdateInfo::fromArray($response);
|
||||||
|
|
||||||
|
if ($this->cache !== null) {
|
||||||
|
$item = $this->cache->getItem($cacheKey);
|
||||||
|
$item->set($result);
|
||||||
|
$item->expiresAfter($this->cacheTtl);
|
||||||
|
$this->cache->save($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Update check completed', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'update_available' => $result->updateAvailable,
|
||||||
|
'version' => $result->version,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws LicenseException
|
* @throws LicenseException
|
||||||
* @throws SignatureException
|
* @throws SignatureException
|
||||||
|
|||||||
272
tmp/openapi.json
272
tmp/openapi.json
@@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "WooCommerce Licensed Product API",
|
"title": "WooCommerce Licensed Product API",
|
||||||
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.\n\n## Response Signing (Optional)\n\nWhen the server is configured with `WC_LICENSE_SERVER_SECRET`, all API responses include cryptographic signatures for tamper protection:\n\n- `X-License-Signature`: HMAC-SHA256 signature of the response\n- `X-License-Timestamp`: Unix timestamp when the response was generated\n\nSignature verification prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` library's `SecureLicenseClient` class to automatically verify signatures.",
|
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.\n\n## Response Signing (Optional)\n\nWhen the server is configured with `WC_LICENSE_SERVER_SECRET`, all API responses include cryptographic signatures for tamper protection:\n\n- `X-License-Signature`: HMAC-SHA256 signature of the response\n- `X-License-Timestamp`: Unix timestamp when the response was generated\n\nSignature verification prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` library's `SecureLicenseClient` class to automatically verify signatures.",
|
||||||
"version": "0.3.2",
|
"version": "0.4.0",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Marco Graetsch",
|
"name": "Marco Graetsch",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev",
|
"url": "https://src.bundespruefstelle.ch/magdev",
|
||||||
@@ -332,6 +332,148 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/update-check": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "checkForUpdates",
|
||||||
|
"summary": "Check for plugin updates",
|
||||||
|
"description": "Checks if a newer version of the licensed product is available. Returns WordPress-compatible update information that can be used to integrate with WordPress's native plugin update system.",
|
||||||
|
"tags": ["Plugin Updates"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678",
|
||||||
|
"domain": "example.com",
|
||||||
|
"plugin_slug": "my-licensed-plugin",
|
||||||
|
"current_version": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Update check completed successfully",
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"$ref": "#/components/headers/X-License-Signature"
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"$ref": "#/components/headers/X-License-Timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"update_available": {
|
||||||
|
"summary": "Update is available",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"update_available": true,
|
||||||
|
"version": "1.2.0",
|
||||||
|
"slug": "my-licensed-plugin",
|
||||||
|
"plugin": "my-licensed-plugin/my-licensed-plugin.php",
|
||||||
|
"download_url": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"package": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"last_updated": "2026-01-27",
|
||||||
|
"tested": "6.7",
|
||||||
|
"requires": "6.0",
|
||||||
|
"requires_php": "8.3",
|
||||||
|
"changelog": "## 1.2.0\n- New feature added\n- Bug fixes",
|
||||||
|
"package_hash": "sha256:abc123def456...",
|
||||||
|
"name": "My Licensed Plugin",
|
||||||
|
"homepage": "https://example.com/product/my-plugin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"no_update": {
|
||||||
|
"summary": "No update available",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"update_available": false,
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "License validation failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_invalid": {
|
||||||
|
"summary": "License is not valid",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "license_invalid",
|
||||||
|
"message": "License validation failed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain_mismatch": {
|
||||||
|
"summary": "Domain mismatch",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "domain_mismatch",
|
||||||
|
"message": "This license is not valid for this domain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "License or product not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_not_found": {
|
||||||
|
"summary": "License not found",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "license_not_found",
|
||||||
|
"message": "License not found."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"product_not_found": {
|
||||||
|
"summary": "Product not found",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "product_not_found",
|
||||||
|
"message": "Licensed product not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"$ref": "#/components/responses/RateLimitExceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -473,6 +615,130 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"UpdateCheckRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["license_key", "domain"],
|
||||||
|
"properties": {
|
||||||
|
"license_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The license key to validate (format: XXXX-XXXX-XXXX-XXXX)",
|
||||||
|
"maxLength": 64,
|
||||||
|
"example": "ABCD-1234-EFGH-5678"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain the plugin is installed on",
|
||||||
|
"maxLength": 255,
|
||||||
|
"example": "example.com"
|
||||||
|
},
|
||||||
|
"plugin_slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The plugin slug (optional, for identification)",
|
||||||
|
"example": "my-licensed-plugin"
|
||||||
|
},
|
||||||
|
"current_version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Currently installed version for comparison",
|
||||||
|
"example": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateCheckResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the request was successful"
|
||||||
|
},
|
||||||
|
"update_available": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether an update is available"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Latest available version"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Plugin slug for WordPress"
|
||||||
|
},
|
||||||
|
"plugin": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Plugin basename (slug/slug.php)"
|
||||||
|
},
|
||||||
|
"download_url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Secure download URL for the update package"
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Alias for download_url (WordPress compatibility)"
|
||||||
|
},
|
||||||
|
"last_updated": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"description": "Date of the latest release"
|
||||||
|
},
|
||||||
|
"tested": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Highest WordPress version tested with"
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Minimum required WordPress version"
|
||||||
|
},
|
||||||
|
"requires_php": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Minimum required PHP version"
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Release notes/changelog for the update"
|
||||||
|
},
|
||||||
|
"package_hash": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SHA256 hash of the package for integrity verification",
|
||||||
|
"example": "sha256:abc123..."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Product name"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Product homepage URL"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Plugin icons for WordPress admin",
|
||||||
|
"properties": {
|
||||||
|
"1x": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"2x": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Content sections for plugin info modal",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ErrorResponse": {
|
"ErrorResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -577,6 +843,10 @@
|
|||||||
{
|
{
|
||||||
"name": "License Activation",
|
"name": "License Activation",
|
||||||
"description": "Activate licenses on domains"
|
"description": "Activate licenses on domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Plugin Updates",
|
||||||
|
"description": "Check for plugin updates"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user