9 Commits

Author SHA1 Message Date
f7490de69b Release v0.2.2 - Display file checksums in UI
Features:
- Add SHA256 column to admin product versions table
- Display file hash in customer account downloads section
- Style checksum file upload field consistently with package upload

Changes:
- Admin versions table shows truncated hash with full hash on hover
- Customer downloads show hash with shield icon indicator
- Updated German translations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:35:25 +01:00
d2bf9aa330 Style checksum file upload field to match package upload field
- Changed plain file input to styled button with filename display
- Added Select/Remove buttons for checksum file upload
- Updated JavaScript handlers for styled checksum file input
- Updated German translation for new button text

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:26:48 +01:00
d00a2235ef Clean up roadmap after v0.2.1 release
- Remove known bug (checksum field issue was fixed)
- Remove completed v0.2.1 tasks from roadmap
- Add v0.2.1 version link to CHANGELOG

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:22:59 +01:00
27c9a22739 Add v0.2.1 release package
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:16:44 +01:00
fc2fe70576 v0.2.1: Change SHA256 input to file upload field
- Replace SHA256 text input with file upload field for checksum files
- Add readChecksumFile() JavaScript function using FileReader API
- Support .sha256 and .txt checksum file formats
- Add Promise-based async handling for file reading
- Add localized error messages for checksum file validation
- Update translations (de_CH) with new strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:13:27 +01:00
f5a1e55710 Add v0.2.0 release package
- wc-licensed-product-0.2.0.zip (486 KB)
- SHA256: 20d90f61721b4579cb979cd19b0262f3286c3510dcb0345fe5e8da2703e3836f

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:59:56 +01:00
4aecba3272 Merge branch 'dev' 2026-01-22 16:57:58 +01:00
23bbc24c5f Release v0.2.0 - Security and integrity features
- Add REST API response signing using HMAC-SHA256
- Add SHA256 hash validation for version file uploads
- Add ResponseSigner class for automatic API response signing
- Add file_hash column to database schema
- Remove external URL support from version uploads
- Update translations with all fuzzy strings resolved

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:57:54 +01:00
8420734f37 Update CLAUDE.md with v0.1.0 session history
- Removed completed 0.1.0 roadmap items
- Added comprehensive session history for v0.1.0 release
- Documented code review findings and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 12:00:01 +01:00
22 changed files with 1288 additions and 357 deletions

View File

@@ -7,6 +7,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.2] - 2026-01-22
### Added
- SHA256 checksum column in admin product versions table
- File hash display in customer account downloads section
- Visual indicators for file integrity verification
### Changed
- Checksum file upload field now styled consistently with package upload field
- Download list items now show truncated hash with full hash on hover
### Technical Details
- ProductVersion `getFileHash()` method now exposed in admin and frontend views
- Frontend CSS extended with `.download-hash` styles
- Admin CSS extended with `.file-hash` styles
## [0.2.1] - 2026-01-22
### Changed
- SHA256 hash input changed from text field to file upload field
- Checksum files (.sha256 or .txt) can now be uploaded directly
- Improved user experience for version integrity verification
### Technical Details
- Added `readChecksumFile()` JavaScript function using FileReader API with Promise support
- Checksum file format supports both "hash filename" and plain "hash" formats
- Added localized error messages for checksum file validation
## [0.2.0] - 2026-01-22
### Added
- Response signing for REST API using HMAC-SHA256
- SHA256 hash field for product version uploads with checksum validation
- File integrity verification before storing uploaded version files
- New `ResponseSigner` class for automatic API response signing
- Database column `file_hash` in versions table for storing checksums
### Changed
- Version uploads now require file attachments (external URL option removed)
- API responses now include `X-License-Signature` and `X-License-Timestamp` headers when `WC_LICENSE_SERVER_SECRET` is configured
### Removed
- External download URL field from product version form
- Direct URL support in version uploads (use Media Library uploads only)
### Security
- API response signing prevents tampering and replay attacks
- Per-license key derivation using HKDF-like approach
- SHA256 checksum validation ensures file integrity
### Technical Details
- New class: `ResponseSigner` for HMAC-SHA256 response signing
- VersionManager extended with `$fileHash` parameter and validation
- ProductVersion model extended with `fileHash` property
- Signature algorithm: `HMAC-SHA256(derived_key, timestamp + ':' + canonical_json)`
- Key derivation: `HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret)`
- Compatible with `magdev/wc-licensed-product-client` SecureLicenseClient
### Configuration
To enable response signing, add to `wp-config.php`:
```php
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
```
## [0.1.0] - 2026-01-22 ## [0.1.0] - 2026-01-22
### Added ### Added
@@ -297,7 +373,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- WordPress REST API integration - WordPress REST API integration
- Custom WooCommerce product type extending WC_Product - Custom WooCommerce product type extending WC_Product
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.1.0...HEAD [Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.2...HEAD
[0.2.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.1...v0.2.2
[0.2.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.0...v0.2.1
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.1.0...v0.2.0
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.11...v0.1.0 [0.1.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.11...v0.1.0
[0.0.11]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.10...v0.0.11 [0.0.11]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.10...v0.0.11
[0.0.10]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.9...v0.0.10 [0.0.10]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.9...v0.0.10

109
CLAUDE.md
View File

@@ -34,14 +34,13 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
### Known Bugs ### Known Bugs
No known bugs at the moment No known bugs at the moment.
### Release 0.1.0 ### Version 0.3.0
- Check the code for wordpress best practices, WooCommerce best practices and common security pitfalls. Refactor if needed. - Implement License check using the composer package `magdev/wc-licensed-product-client` located in the local folder `/home/magdev/workspaces/php/wc-licensed-product-client`
- Update the README.md according to the current featureset - Add license configuration to the plugins settings page
- Update all translations - Hide frontend parts if no valid license is provided
- Create a release package 0.1.0
## Technical Stack ## Technical Stack
@@ -624,3 +623,101 @@ Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
- Created release package: `releases/wc-licensed-product-0.0.11.zip` (473 KB) - Created release package: `releases/wc-licensed-product-0.0.11.zip` (473 KB)
- SHA256: `c3f66c4ac54741053f87ce1a63b4ddb49ad9707d5c194a271311bb95518ab13c` - SHA256: `c3f66c4ac54741053f87ce1a63b4ddb49ad9707d5c194a271311bb95518ab13c`
- Tagged as `v0.0.11` and pushed to `main` branch - Tagged as `v0.0.11` and pushed to `main` branch
### 2026-01-22 - Version 0.1.0 - First Stable Minor Release
**Overview:**
First stable minor release after comprehensive code review for WordPress/WooCommerce best practices and security.
**Code Review Findings:**
Security practices verified:
- Input sanitization with `sanitize_text_field()`, `absint()`, `esc_attr()`, `esc_html()`, `esc_url()`
- Nonce verification on all forms and AJAX handlers
- Capability checks with `current_user_can('manage_woocommerce')`
- SQL injection prevention using `$wpdb->prepare()` throughout
- Secure download URLs with hash verification using `hash_equals()`
- Rate limiting on REST API (30 requests/minute)
- Cryptographically secure license key generation with `random_int()`
**Bug Fixes:**
- Fixed `VersionManager::updateVersion()` null format handling for attachment ID updates
- Improved input sanitization in `AdminController::enqueueAdminAssets()` for page context checks
**Documentation Updates:**
- Updated README.md with complete feature documentation
- Added new features: Live Search, Inline Editing, Order Integration, WooCommerce HPOS compatibility, Checkout Blocks support
- Removed outdated "Current Version" field from usage instructions
**Translation Updates:**
- Regenerated .pot template with all current strings
- Updated German (de_CH) translation with new strings
- Compiled .mo file for production use
**Modified files:**
- `src/Product/VersionManager.php` - Fixed null format handling in attachment update
- `src/Admin/AdminController.php` - Improved $_GET sanitization for page context
- `README.md` - Updated feature documentation
- `CHANGELOG.md` - Added 0.1.0 release notes
- `wc-licensed-product.php` - Version bump to 0.1.0
- `languages/*` - Updated all translation files
**Release v0.1.0:**
- Created release package: `releases/wc-licensed-product-0.1.0.zip` (478 KB)
- SHA256: `62638e240315107098be4cb40faff8395e9e1b719d79b73d80e69d680b305e87`
- Tagged as `v0.1.0` and pushed to `main` branch
### 2026-01-22 - Version 0.2.0 - Security & Integrity Features
**Overview:**
Added response signing for REST API security and SHA256 checksum validation for uploaded version files.
**Implemented:**
- REST API response signing using HMAC-SHA256 for tamper-proof responses
- SHA256 hash field for product version uploads with server-side validation
- Per-license key derivation using HKDF-like approach
- Automatic signature headers on license API endpoints
**Removed:**
- External download URL field from product version form
- Direct URL support in version uploads (Media Library only now)
**New files:**
- `src/Api/ResponseSigner.php` - HMAC-SHA256 response signing class
**Modified files:**
- `src/Installer.php` - Added `file_hash` column to versions table schema
- `src/Product/ProductVersion.php` - Added `fileHash` property and getter
- `src/Product/VersionManager.php` - Removed `$downloadUrl` parameter, added `$fileHash` with validation
- `src/Admin/VersionAdminController.php` - Removed URL field, added SHA256 hash field
- `assets/js/versions.js` - Updated form handling for hash field
- `src/Plugin.php` - Initialize ResponseSigner when server secret is configured
**Technical notes:**
- Response signing only activates when `WC_LICENSE_SERVER_SECRET` constant is defined
- Signature algorithm: `HMAC-SHA256(derived_key, timestamp + ':' + canonical_json)`
- Key derivation: `HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret)`
- Hash validation throws `InvalidArgumentException` on mismatch
- Compatible with `magdev/wc-licensed-product-client` SecureLicenseClient
- Database migration handled by WordPress `dbDelta()` function
**Configuration:**
To enable response signing, add to `wp-config.php`:
```php
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
```

View File

@@ -43,6 +43,13 @@
font-size: 0.9em; font-size: 0.9em;
} }
/* File Hash */
code.file-hash {
cursor: help;
font-size: 0.85em;
color: #666;
}
/* License Product Tab */ /* License Product Tab */
#woocommerce-product-data .show_if_licensed { #woocommerce-product-data .show_if_licensed {
display: block !important; display: block !important;

View File

@@ -247,6 +247,30 @@
margin-left: auto; margin-left: auto;
} }
.download-hash {
display: inline-flex;
align-items: center;
gap: 0.25em;
font-size: 0.8em;
color: #666;
}
.download-hash .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
color: #28a745;
}
.download-hash code {
font-family: 'SF Mono', Monaco, Consolas, monospace;
background: #f5f5f5;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
color: #666;
}
/* Domain Field */ /* Domain Field */
#licensed-product-domain-field { #licensed-product-domain-field {
margin-top: 2em; margin-top: 2em;

View File

@@ -23,6 +23,11 @@
$('#upload-version-file-btn').on('click', this.openMediaUploader.bind(this)); $('#upload-version-file-btn').on('click', this.openMediaUploader.bind(this));
$('#remove-version-file-btn').on('click', this.removeSelectedFile); $('#remove-version-file-btn').on('click', this.removeSelectedFile);
// Checksum file events
$('#select-checksum-file-btn').on('click', this.triggerChecksumFileSelect);
$('#new_checksum_file').on('change', this.onChecksumFileSelected);
$('#remove-checksum-file-btn').on('click', this.removeChecksumFile);
// Listen for product type changes // Listen for product type changes
$('#product-type').on('change', this.onProductTypeChange); $('#product-type').on('change', this.onProductTypeChange);
@@ -78,14 +83,14 @@
$('#selected_file_name').text(attachment.filename); $('#selected_file_name').text(attachment.filename);
$('#remove-version-file-btn').show(); $('#remove-version-file-btn').show();
// Show SHA256 hash field
$('#sha256-hash-row').show();
// Try to extract version from filename // Try to extract version from filename
var extractedVersion = self.extractVersionFromFilename(attachment.filename); var extractedVersion = self.extractVersionFromFilename(attachment.filename);
if (extractedVersion && !$('#new_version').val().trim()) { if (extractedVersion && !$('#new_version').val().trim()) {
$('#new_version').val(extractedVersion); $('#new_version').val(extractedVersion);
} }
// Clear external URL when file is selected
$('#new_download_url').val('');
}); });
this.mediaFrame.open(); this.mediaFrame.open();
@@ -100,6 +105,73 @@
$('#new_attachment_id').val(''); $('#new_attachment_id').val('');
$('#selected_file_name').text(''); $('#selected_file_name').text('');
$('#remove-version-file-btn').hide(); $('#remove-version-file-btn').hide();
// Hide and clear checksum file field
$('#sha256-hash-row').hide();
$('#new_checksum_file').val('');
$('#selected_checksum_name').text('');
$('#remove-checksum-file-btn').hide();
},
/**
* Trigger checksum file input click
*/
triggerChecksumFileSelect: function(e) {
e.preventDefault();
$('#new_checksum_file').trigger('click');
},
/**
* Handle checksum file selection
*/
onChecksumFileSelected: function(e) {
var file = e.target.files[0];
if (file) {
$('#selected_checksum_name').text(file.name);
$('#remove-checksum-file-btn').show();
} else {
$('#selected_checksum_name').text('');
$('#remove-checksum-file-btn').hide();
}
},
/**
* Remove selected checksum file
*/
removeChecksumFile: function(e) {
e.preventDefault();
$('#new_checksum_file').val('');
$('#selected_checksum_name').text('');
$('#remove-checksum-file-btn').hide();
},
/**
* Read checksum from uploaded file
* Supports formats: "hash filename" or just "hash"
*/
readChecksumFile: function(file) {
return new Promise(function(resolve, reject) {
if (!file) {
resolve('');
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var content = e.target.result.trim();
// Extract hash from content (format: "hash filename" or just "hash")
var match = content.match(/^([a-fA-F0-9]{64})/);
if (match) {
resolve(match[1].toLowerCase());
} else {
reject(new Error(wcLicensedProductVersions.strings.invalidChecksumFile || 'Invalid checksum file format'));
}
};
reader.onerror = function() {
reject(new Error(wcLicensedProductVersions.strings.checksumReadError || 'Failed to read checksum file'));
};
reader.readAsText(file);
});
}, },
/** /**
@@ -130,13 +202,14 @@
addVersion: function(e) { addVersion: function(e) {
e.preventDefault(); e.preventDefault();
var self = WCLicensedProductVersions;
var $btn = $(this); var $btn = $(this);
var $spinner = $btn.siblings('.spinner'); var $spinner = $btn.siblings('.spinner');
var productId = $btn.data('product-id'); var productId = $btn.data('product-id');
var version = $('#new_version').val().trim(); var version = $('#new_version').val().trim();
var downloadUrl = $('#new_download_url').val().trim();
var releaseNotes = $('#new_release_notes').val().trim(); var releaseNotes = $('#new_release_notes').val().trim();
var attachmentId = $('#new_attachment_id').val(); var attachmentId = $('#new_attachment_id').val();
var checksumFile = $('#new_checksum_file')[0].files[0];
// Validate version // Validate version
if (!version) { if (!version) {
@@ -152,6 +225,8 @@
$btn.prop('disabled', true); $btn.prop('disabled', true);
$spinner.addClass('is-active'); $spinner.addClass('is-active');
// Read checksum file if provided, then submit
self.readChecksumFile(checksumFile).then(function(fileHash) {
$.ajax({ $.ajax({
url: wcLicensedProductVersions.ajaxUrl, url: wcLicensedProductVersions.ajaxUrl,
type: 'POST', type: 'POST',
@@ -160,9 +235,9 @@
nonce: wcLicensedProductVersions.nonce, nonce: wcLicensedProductVersions.nonce,
product_id: productId, product_id: productId,
version: version, version: version,
download_url: downloadUrl,
release_notes: releaseNotes, release_notes: releaseNotes,
attachment_id: attachmentId attachment_id: attachmentId,
file_hash: fileHash
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -174,11 +249,14 @@
// Clear form // Clear form
$('#new_version').val(''); $('#new_version').val('');
$('#new_download_url').val('');
$('#new_release_notes').val(''); $('#new_release_notes').val('');
$('#new_attachment_id').val(''); $('#new_attachment_id').val('');
$('#selected_file_name').text(''); $('#selected_file_name').text('');
$('#remove-version-file-btn').hide(); $('#remove-version-file-btn').hide();
$('#sha256-hash-row').hide();
$('#new_checksum_file').val('');
$('#selected_checksum_name').text('');
$('#remove-checksum-file-btn').hide();
} else { } else {
alert(response.data.message || wcLicensedProductVersions.strings.error); alert(response.data.message || wcLicensedProductVersions.strings.error);
} }
@@ -191,6 +269,11 @@
$spinner.removeClass('is-active'); $spinner.removeClass('is-active');
} }
}); });
}).catch(function(error) {
alert(error.message);
$btn.prop('disabled', false);
$spinner.removeClass('is-active');
});
}, },
deleteVersion: function(e) { deleteVersion: function(e) {

View File

@@ -0,0 +1,393 @@
# Server-Side Response Signing Implementation
This document describes how to implement response signing on the server side (e.g., in the WooCommerce Licensed Product plugin) to work with the `SecureLicenseClient`.
## Overview
The security model works as follows:
1. Server generates a unique signature for each response using HMAC-SHA256
2. Signature includes a timestamp to prevent replay attacks
3. Client verifies the signature using a shared secret
4. Invalid signatures cause the client to reject the response
This prevents attackers from:
- Faking valid license responses
- Replaying old responses
- Tampering with response data
## Requirements
- PHP 7.4+ (8.0+ recommended)
- A server secret stored securely (not in version control)
## Server Configuration
### 1. Store the Server Secret
Add a secret key to your WordPress configuration:
```php
// wp-config.php or secure configuration file
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
```
Generate a secure secret:
```bash
# Using OpenSSL
openssl rand -hex 32
# Or using PHP
php -r "echo bin2hex(random_bytes(32));"
```
**IMPORTANT:** Never commit this secret to version control!
## Implementation
### Key Derivation
Each license key gets a unique signing key derived from the server secret:
```php
/**
* Derive a unique signing key for a license.
*
* @param string $licenseKey The license key
* @param string $serverSecret The server's master secret
* @return string The derived key (hex encoded)
*/
function derive_signing_key(string $licenseKey, string $serverSecret): string
{
// HKDF-like key derivation
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
}
```
### Response Signing
Sign every API response before sending:
```php
/**
* Sign an API response.
*
* @param array $responseData The response body (before JSON encoding)
* @param string $licenseKey The license key from the request
* @param string $serverSecret The server's master secret
* @return array Headers to add to the response
*/
function sign_response(array $responseData, string $licenseKey, string $serverSecret): array
{
$timestamp = time();
$signingKey = derive_signing_key($licenseKey, $serverSecret);
// Sort keys for consistent ordering
ksort($responseData);
// Build signature payload
$jsonBody = json_encode($responseData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$payload = $timestamp . ':' . $jsonBody;
// Generate HMAC signature
$signature = hash_hmac('sha256', $payload, $signingKey);
return [
'X-License-Signature' => $signature,
'X-License-Timestamp' => (string) $timestamp,
];
}
```
### WordPress REST API Integration
Example integration with WooCommerce REST API:
```php
/**
* Add signature headers to license API responses.
*/
add_filter('rest_post_dispatch', function($response, $server, $request) {
// Only sign license API responses
if (!str_starts_with($request->get_route(), '/wc-licensed-product/v1/')) {
return $response;
}
// Get the response data
$data = $response->get_data();
// Get the license key from the request
$licenseKey = $request->get_param('license_key');
if (empty($licenseKey) || !is_array($data)) {
return $response;
}
// Sign the response
$serverSecret = defined('WC_LICENSE_SERVER_SECRET')
? WC_LICENSE_SERVER_SECRET
: '';
if (empty($serverSecret)) {
// Log warning: server secret not configured
return $response;
}
$signatureHeaders = sign_response($data, $licenseKey, $serverSecret);
// Add headers to response
foreach ($signatureHeaders as $name => $value) {
$response->header($name, $value);
}
return $response;
}, 10, 3);
```
### Complete WordPress Plugin Example
```php
<?php
/**
* Plugin Name: WC Licensed Product Signature
* Description: Adds response signing to WC Licensed Product API
* Version: 1.0.0
*/
namespace WcLicensedProduct\Security;
class ResponseSigner
{
private string $serverSecret;
public function __construct()
{
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET')
? WC_LICENSE_SERVER_SECRET
: '';
}
public function register(): void
{
add_filter('rest_post_dispatch', [$this, 'signResponse'], 10, 3);
}
public function signResponse($response, $server, $request)
{
if (!$this->shouldSign($request)) {
return $response;
}
$data = $response->get_data();
$licenseKey = $request->get_param('license_key');
if (empty($licenseKey) || !is_array($data) || empty($this->serverSecret)) {
return $response;
}
$headers = $this->createSignatureHeaders($data, $licenseKey);
foreach ($headers as $name => $value) {
$response->header($name, $value);
}
return $response;
}
private function shouldSign($request): bool
{
$route = $request->get_route();
return str_starts_with($route, '/wc-licensed-product/v1/validate')
|| str_starts_with($route, '/wc-licensed-product/v1/status')
|| str_starts_with($route, '/wc-licensed-product/v1/activate');
}
private function createSignatureHeaders(array $data, string $licenseKey): array
{
$timestamp = time();
$signingKey = $this->deriveKey($licenseKey);
ksort($data);
$payload = $timestamp . ':' . json_encode(
$data,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
return [
'X-License-Signature' => hash_hmac('sha256', $payload, $signingKey),
'X-License-Timestamp' => (string) $timestamp,
];
}
private function deriveKey(string $licenseKey): string
{
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
}
}
// Initialize
add_action('init', function() {
(new ResponseSigner())->register();
});
```
## Response Format
### Headers
Every signed response includes:
| Header | Description | Example |
| -------- | ------------- | --------- |
| `X-License-Signature` | HMAC-SHA256 signature (hex) | `a1b2c3d4...` (64 chars) |
| `X-License-Timestamp` | Unix timestamp when signed | `1706000000` |
### Signature Algorithm
```text
signature = HMAC-SHA256(
key = derive_signing_key(license_key, server_secret),
message = timestamp + ":" + canonical_json(response_body)
)
```
Where:
- `derive_signing_key` uses HKDF-like derivation (see above)
- `canonical_json` sorts keys alphabetically, no escaping of slashes/unicode
- Result is hex-encoded (64 characters)
## Testing
### Verify Signing Works
```php
// Test script
$serverSecret = 'test-secret-key-for-development-only';
$licenseKey = 'ABCD-1234-EFGH-5678';
$responseData = [
'valid' => true,
'license' => [
'product_id' => 123,
'expires_at' => '2027-01-21',
'version_id' => null,
],
];
$headers = sign_response($responseData, $licenseKey, $serverSecret);
echo "X-License-Signature: " . $headers['X-License-Signature'] . "\n";
echo "X-License-Timestamp: " . $headers['X-License-Timestamp'] . "\n";
```
### Test with Client
```php
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://your-site.com',
serverSecret: 'same-secret-as-server',
);
try {
$info = $client->validate('ABCD-1234-EFGH-5678', 'example.com');
echo "License valid! Product ID: " . $info->productId;
} catch (SignatureException $e) {
echo "Signature verification failed - possible tampering!";
}
```
## Security Considerations
### Timestamp Tolerance
The client allows a 5-minute window for timestamp verification. This:
- Prevents replay attacks (old responses rejected)
- Allows for reasonable clock skew between server and client
Adjust if needed:
```php
// Client-side: custom tolerance
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
```
### Secret Key Rotation
To rotate the server secret:
1. Deploy new secret to server
2. Update client configurations
3. Old signatures become invalid immediately
For zero-downtime rotation, implement versioned secrets:
```php
// Server supports both old and new secrets during transition
$secrets = [
'v2' => 'new-secret',
'v1' => 'old-secret',
];
// Add version to signature header
$response->header('X-License-Signature-Version', 'v2');
```
### Error Responses
Sign error responses too! Otherwise attackers could craft fake error messages:
```php
// Sign both success and error responses
$errorData = [
'valid' => false,
'error' => 'license_expired',
'message' => 'This license has expired.',
];
$headers = sign_response($errorData, $licenseKey, $serverSecret);
```
## Troubleshooting
### "Response is not signed by the server"
- Server not configured with `WC_LICENSE_SERVER_SECRET`
- Filter not registered (check plugin activation)
- Route mismatch (check `shouldSign()` paths)
### "Response signature verification failed"
- Different secrets on server/client
- Clock skew > 5 minutes
- Response body modified after signing (e.g., by caching plugin)
- JSON encoding differences (check `ksort` and flags)
### Debugging
Enable detailed logging:
```php
// Server-side
error_log('Signing response for: ' . $licenseKey);
error_log('Timestamp: ' . $timestamp);
error_log('Payload: ' . $payload);
error_log('Signature: ' . $signature);
// Client-side: use a PSR-3 logger
$client = new SecureLicenseClient(
// ...
logger: new YourDebugLogger(),
);
```

View File

@@ -3,10 +3,10 @@
# This file is distributed under the GPL-2.0-or-later. # This file is distributed under the GPL-2.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WC Licensed Product 0.1.0\n" "Project-Id-Version: WC Licensed Product 0.2.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-22 11:52+0100\n" "POT-Creation-Date: 2026-01-22 17:32+0100\n"
"PO-Revision-Date: 2026-01-21T00:00:00+00:00\n" "PO-Revision-Date: 2026-01-22T17:15: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"
"Language: de_CH\n" "Language: de_CH\n"
@@ -15,7 +15,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. translators: %s: WooCommerce plugin name
#: wc-licensed-product.php:61 #: wc-licensed-product.php:61
#, php-format #, php-format
msgid "%s requires WooCommerce to be installed and active." msgid "%s requires WooCommerce to be installed and active."
@@ -44,18 +43,16 @@ msgid "Overview"
msgstr "Übersicht" msgstr "Übersicht"
#: src/Admin/AdminController.php:138 #: src/Admin/AdminController.php:138
#, fuzzy
msgid "No licenses found" msgid "No licenses found"
msgstr "Keine Lizenzen gefunden." msgstr "Keine Lizenzen gefunden"
#: src/Admin/AdminController.php:139 #: src/Admin/AdminController.php:139
msgid "Searching..." msgid "Searching..."
msgstr "Suche..." msgstr "Suche..."
#: src/Admin/AdminController.php:140 #: src/Admin/AdminController.php:140
#, fuzzy
msgid "Search failed" msgid "Search failed"
msgstr "Speichern fehlgeschlagen" msgstr "Suche fehlgeschlagen"
#: src/Admin/AdminController.php:141 src/Admin/OrderLicenseController.php:285 #: src/Admin/AdminController.php:141 src/Admin/OrderLicenseController.php:285
msgid "Saving..." msgid "Saving..."
@@ -84,7 +81,7 @@ msgstr "Bearbeiten"
#: src/Admin/AdminController.php:146 src/Admin/AdminController.php:1303 #: src/Admin/AdminController.php:146 src/Admin/AdminController.php:1303
#: src/Admin/AdminController.php:1323 src/Admin/AdminController.php:1344 #: src/Admin/AdminController.php:1323 src/Admin/AdminController.php:1344
#: src/Admin/OrderLicenseController.php:185 #: src/Admin/OrderLicenseController.php:185
#: src/Frontend/AccountController.php:270 #: src/Frontend/AccountController.php:271
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
@@ -102,25 +99,25 @@ msgstr "Speichern"
msgid "Lifetime" msgid "Lifetime"
msgstr "Lebenslang" msgstr "Lebenslang"
#: src/Admin/AdminController.php:149 src/Frontend/AccountController.php:308 #: src/Admin/AdminController.php:149 src/Frontend/AccountController.php:309
msgid "Copied!" msgid "Copied!"
msgstr "Kopiert!" msgstr "Kopiert!"
#: src/Admin/AdminController.php:150 src/Frontend/AccountController.php:309 #: src/Admin/AdminController.php:150 src/Frontend/AccountController.php:310
msgid "Copy failed" msgid "Copy failed"
msgstr "Kopieren fehlgeschlagen" msgstr "Kopieren fehlgeschlagen"
#: src/Admin/AdminController.php:153 src/Admin/AdminController.php:875 #: src/Admin/AdminController.php:153 src/Admin/AdminController.php:875
#: src/Admin/AdminController.php:1194 src/Admin/AdminController.php:1317 #: src/Admin/AdminController.php:1194 src/Admin/AdminController.php:1317
#: src/Admin/VersionAdminController.php:165 #: src/Admin/VersionAdminController.php:180
#: src/Admin/VersionAdminController.php:381 #: src/Admin/VersionAdminController.php:409
msgid "Active" msgid "Active"
msgstr "Aktiv" msgstr "Aktiv"
#: src/Admin/AdminController.php:154 src/Admin/AdminController.php:882 #: src/Admin/AdminController.php:154 src/Admin/AdminController.php:882
#: src/Admin/AdminController.php:1195 src/Admin/AdminController.php:1318 #: src/Admin/AdminController.php:1195 src/Admin/AdminController.php:1318
#: src/Admin/VersionAdminController.php:165 #: src/Admin/VersionAdminController.php:180
#: src/Admin/VersionAdminController.php:381 #: src/Admin/VersionAdminController.php:409
msgid "Inactive" msgid "Inactive"
msgstr "Inaktiv" msgstr "Inaktiv"
@@ -138,9 +135,9 @@ msgstr "Widerrufen"
#: src/Admin/AdminController.php:246 src/Admin/AdminController.php:298 #: src/Admin/AdminController.php:246 src/Admin/AdminController.php:298
#: src/Admin/AdminController.php:336 src/Admin/OrderLicenseController.php:301 #: src/Admin/AdminController.php:336 src/Admin/OrderLicenseController.php:301
#: src/Admin/OrderLicenseController.php:340 #: src/Admin/OrderLicenseController.php:340
#: src/Admin/VersionAdminController.php:240 #: src/Admin/VersionAdminController.php:257
#: src/Admin/VersionAdminController.php:305 #: src/Admin/VersionAdminController.php:326
#: src/Admin/VersionAdminController.php:331 #: src/Admin/VersionAdminController.php:352
msgid "Permission denied." msgid "Permission denied."
msgstr "Zugriff verweigert." msgstr "Zugriff verweigert."
@@ -211,7 +208,7 @@ msgstr "Lizenz konnte nicht widerrufen werden."
#: src/Admin/AdminController.php:466 src/Admin/AdminController.php:484 #: src/Admin/AdminController.php:466 src/Admin/AdminController.php:484
#: src/Admin/AdminController.php:504 src/Admin/AdminController.php:522 #: src/Admin/AdminController.php:504 src/Admin/AdminController.php:522
#: src/Admin/AdminController.php:589 src/Admin/AdminController.php:779 #: src/Admin/AdminController.php:589 src/Admin/AdminController.php:779
#: src/Frontend/AccountController.php:325 #: src/Frontend/AccountController.php:326
msgid "Security check failed." msgid "Security check failed."
msgstr "Sicherheitsüberprüfung fehlgeschlagen." msgstr "Sicherheitsüberprüfung fehlgeschlagen."
@@ -249,11 +246,11 @@ msgid "Attention:"
msgstr "Achtung:" msgstr "Achtung:"
#: src/Admin/AdminController.php:910 #: src/Admin/AdminController.php:910
#, fuzzy, php-format #, php-format
msgid "%d license is expiring within the next 30 days." msgid "%d license is expiring within the next 30 days."
msgid_plural "%d licenses are expiring within the next 30 days." msgid_plural "%d licenses are expiring within the next 30 days."
msgstr[0] "läuft innerhalb der nächsten 30 Tage ab." msgstr[0] "%d Lizenz läuft innerhalb der nächsten 30 Tage ab."
msgstr[1] "läuft innerhalb der nächsten 30 Tage ab." msgstr[1] "%d Lizenzen laufen innerhalb der nächsten 30 Tage ab."
#: src/Admin/AdminController.php:918 #: src/Admin/AdminController.php:918
msgid "View Licenses" msgid "View Licenses"
@@ -287,7 +284,6 @@ msgstr "Lizenz erfolgreich verlängert."
msgid "License set to lifetime successfully." msgid "License set to lifetime successfully."
msgstr "Lizenz erfolgreich auf lebenslang gesetzt." msgstr "Lizenz erfolgreich auf lebenslang gesetzt."
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1068 #: src/Admin/AdminController.php:1068
#, php-format #, php-format
msgid "%d license activated." msgid "%d license activated."
@@ -295,7 +291,6 @@ msgid_plural "%d licenses activated."
msgstr[0] "%d Lizenz aktiviert." msgstr[0] "%d Lizenz aktiviert."
msgstr[1] "%d Lizenzen aktiviert." msgstr[1] "%d Lizenzen aktiviert."
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1076 #: src/Admin/AdminController.php:1076
#, php-format #, php-format
msgid "%d license deactivated." msgid "%d license deactivated."
@@ -303,7 +298,6 @@ msgid_plural "%d licenses deactivated."
msgstr[0] "%d Lizenz deaktiviert." msgstr[0] "%d Lizenz deaktiviert."
msgstr[1] "%d Lizenzen deaktiviert." msgstr[1] "%d Lizenzen deaktiviert."
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1084 #: src/Admin/AdminController.php:1084
#, php-format #, php-format
msgid "%d license revoked." msgid "%d license revoked."
@@ -311,7 +305,6 @@ msgid_plural "%d licenses revoked."
msgstr[0] "%d Lizenz widerrufen." msgstr[0] "%d Lizenz widerrufen."
msgstr[1] "%d Lizenzen widerrufen." msgstr[1] "%d Lizenzen widerrufen."
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1092 #: src/Admin/AdminController.php:1092
#, php-format #, php-format
msgid "%d license deleted." msgid "%d license deleted."
@@ -319,7 +312,6 @@ msgid_plural "%d licenses deleted."
msgstr[0] "%d Lizenz gelöscht." msgstr[0] "%d Lizenz gelöscht."
msgstr[1] "%d Lizenzen gelöscht." msgstr[1] "%d Lizenzen gelöscht."
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1100 #: src/Admin/AdminController.php:1100
#, php-format #, php-format
msgid "%d license extended." msgid "%d license extended."
@@ -341,7 +333,6 @@ msgstr ""
msgid "No licenses to export." msgid "No licenses to export."
msgstr "Keine Lizenzen zum Exportieren." msgstr "Keine Lizenzen zum Exportieren."
#. translators: %d: number of licenses imported
#: src/Admin/AdminController.php:1121 #: src/Admin/AdminController.php:1121
#, php-format #, php-format
msgid "%d license imported." msgid "%d license imported."
@@ -349,7 +340,6 @@ msgid_plural "%d licenses imported."
msgstr[0] "%d Lizenz importiert." msgstr[0] "%d Lizenz importiert."
msgstr[1] "%d Lizenzen importiert." msgstr[1] "%d Lizenzen importiert."
#. translators: %d: number of licenses updated
#: src/Admin/AdminController.php:1128 #: src/Admin/AdminController.php:1128
#, php-format #, php-format
msgid "%d updated." msgid "%d updated."
@@ -357,7 +347,6 @@ msgid_plural "%d updated."
msgstr[0] "%d aktualisiert." msgstr[0] "%d aktualisiert."
msgstr[1] "%d aktualisiert." msgstr[1] "%d aktualisiert."
#. translators: %d: number of licenses skipped
#: src/Admin/AdminController.php:1136 #: src/Admin/AdminController.php:1136
#, php-format #, php-format
msgid "%d skipped." msgid "%d skipped."
@@ -365,7 +354,6 @@ msgid_plural "%d skipped."
msgstr[0] "%d übersprungen." msgstr[0] "%d übersprungen."
msgstr[1] "%d übersprungen." msgstr[1] "%d übersprungen."
#. translators: %d: number of errors
#: src/Admin/AdminController.php:1144 #: src/Admin/AdminController.php:1144
#, php-format #, php-format
msgid "%d error." msgid "%d error."
@@ -442,14 +430,14 @@ msgid "Bulk Actions"
msgstr "Massenaktionen" msgstr "Massenaktionen"
#: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1407 #: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1407
#: src/Admin/VersionAdminController.php:171 #: src/Admin/VersionAdminController.php:186
#: src/Admin/VersionAdminController.php:387 #: src/Admin/VersionAdminController.php:415
msgid "Activate" msgid "Activate"
msgstr "Aktivieren" msgstr "Aktivieren"
#: src/Admin/AdminController.php:1236 src/Admin/AdminController.php:1408 #: src/Admin/AdminController.php:1236 src/Admin/AdminController.php:1408
#: src/Admin/VersionAdminController.php:171 #: src/Admin/VersionAdminController.php:186
#: src/Admin/VersionAdminController.php:387 #: src/Admin/VersionAdminController.php:415
msgid "Deactivate" msgid "Deactivate"
msgstr "Deaktivieren" msgstr "Deaktivieren"
@@ -471,8 +459,8 @@ msgid "Extend 1 year"
msgstr "1 Jahr verlängern" msgstr "1 Jahr verlängern"
#: src/Admin/AdminController.php:1241 src/Admin/AdminController.php:1377 #: src/Admin/AdminController.php:1241 src/Admin/AdminController.php:1377
#: src/Admin/AdminController.php:1413 src/Admin/VersionAdminController.php:174 #: src/Admin/AdminController.php:1413 src/Admin/VersionAdminController.php:189
#: src/Admin/VersionAdminController.php:390 #: src/Admin/VersionAdminController.php:418
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@@ -504,7 +492,7 @@ msgstr "Domain"
#: src/Admin/AdminController.php:1257 src/Admin/AdminController.php:1395 #: src/Admin/AdminController.php:1257 src/Admin/AdminController.php:1395
#: src/Admin/OrderLicenseController.php:147 #: src/Admin/OrderLicenseController.php:147
#: src/Admin/VersionAdminController.php:132 #: src/Admin/VersionAdminController.php:140
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -520,7 +508,7 @@ msgstr "Läuft ab"
#: src/Admin/AdminController.php:1260 src/Admin/AdminController.php:1398 #: src/Admin/AdminController.php:1260 src/Admin/AdminController.php:1398
#: src/Admin/OrderLicenseController.php:149 #: src/Admin/OrderLicenseController.php:149
#: src/Admin/VersionAdminController.php:134 #: src/Admin/VersionAdminController.php:142
msgid "Actions" msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
@@ -528,7 +516,7 @@ msgstr "Aktionen"
msgid "No licenses found." msgid "No licenses found."
msgstr "Keine Lizenzen gefunden." msgstr "Keine Lizenzen gefunden."
#: src/Admin/AdminController.php:1276 src/Frontend/AccountController.php:193 #: src/Admin/AdminController.php:1276 src/Frontend/AccountController.php:194
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "In Zwischenablage kopieren" msgstr "In Zwischenablage kopieren"
@@ -657,25 +645,21 @@ msgid "No domain specified"
msgstr "Keine Domain angegeben" msgstr "Keine Domain angegeben"
#: src/Admin/OrderLicenseController.php:56 #: src/Admin/OrderLicenseController.php:56
#, fuzzy
msgid "Product Licenses" msgid "Product Licenses"
msgstr "Top-Produkte nach Lizenzen" msgstr "Produktlizenzen"
#: src/Admin/OrderLicenseController.php:77 #: src/Admin/OrderLicenseController.php:77
#: src/Admin/OrderLicenseController.php:313 #: src/Admin/OrderLicenseController.php:313
#, fuzzy
msgid "Order not found." msgid "Order not found."
msgstr "Version nicht gefunden." msgstr "Bestellung nicht gefunden."
#: src/Admin/OrderLicenseController.php:92 #: src/Admin/OrderLicenseController.php:92
#, fuzzy
msgid "This order does not contain licensed products." msgid "This order does not contain licensed products."
msgstr "Version stimmt nicht mit Ihrem lizensierten Produkt überein." msgstr "Diese Bestellung enthält keine lizensierten Produkte."
#: src/Admin/OrderLicenseController.php:106 #: src/Admin/OrderLicenseController.php:106
#, fuzzy
msgid "Order Domain" msgid "Order Domain"
msgstr "Neue Domain" msgstr "Bestellungs-Domain"
#: src/Admin/OrderLicenseController.php:108 #: src/Admin/OrderLicenseController.php:108
msgid "" msgid ""
@@ -686,8 +670,8 @@ msgstr ""
"automatisch bestehende Lizenz-Domains." "automatisch bestehende Lizenz-Domains."
#: src/Admin/OrderLicenseController.php:116 #: src/Admin/OrderLicenseController.php:116
#: src/Checkout/CheckoutController.php:89
#: src/Checkout/CheckoutBlocksIntegration.php:102 #: src/Checkout/CheckoutBlocksIntegration.php:102
#: src/Checkout/CheckoutController.php:89
msgid "example.com" msgid "example.com"
msgstr "beispiel.ch" msgstr "beispiel.ch"
@@ -706,22 +690,24 @@ msgstr ""
#: src/Admin/OrderLicenseController.php:137 #: src/Admin/OrderLicenseController.php:137
msgid "Licenses will be generated when the order is marked as paid/completed." msgid "Licenses will be generated when the order is marked as paid/completed."
msgstr "Lizenzen werden generiert, sobald die Bestellung als bezahlt/abgeschlossen markiert wird." msgstr ""
"Lizenzen werden generiert, sobald die Bestellung als bezahlt/abgeschlossen "
"markiert wird."
#: src/Admin/OrderLicenseController.php:178 #: src/Admin/OrderLicenseController.php:178
msgid "Edit domain" msgid "Edit domain"
msgstr "Domain bearbeiten" msgstr "Domain bearbeiten"
#: src/Admin/OrderLicenseController.php:208 #: src/Admin/OrderLicenseController.php:208
#, fuzzy
msgid "View in Licenses" msgid "View in Licenses"
msgstr "Lizenzen anzeigen" msgstr "In Lizenzen anzeigen"
#. translators: %s: Link to licenses page
#: src/Admin/OrderLicenseController.php:221 #: src/Admin/OrderLicenseController.php:221
#, php-format #, php-format
msgid "For more actions (revoke, extend, delete), go to the %s page." msgid "For more actions (revoke, extend, delete), go to the %s page."
msgstr "Für weitere Aktionen (widerrufen, verlängern, löschen), gehen Sie zur Seite %s." msgstr ""
"Für weitere Aktionen (widerrufen, verlängern, löschen), gehen Sie zur Seite "
"%s."
#: src/Admin/OrderLicenseController.php:286 #: src/Admin/OrderLicenseController.php:286
msgid "Saved!" msgid "Saved!"
@@ -732,37 +718,33 @@ msgid "Error saving. Please try again."
msgstr "Fehler beim Speichern. Bitte versuchen Sie es erneut." msgstr "Fehler beim Speichern. Bitte versuchen Sie es erneut."
#: src/Admin/OrderLicenseController.php:288 #: src/Admin/OrderLicenseController.php:288
#: src/Frontend/AccountController.php:313 #: src/Frontend/AccountController.php:314
#: src/Frontend/AccountController.php:345 #: src/Frontend/AccountController.php:346
msgid "Please enter a valid domain." msgid "Please enter a valid domain."
msgstr "Bitte geben Sie eine gültige Domain ein." msgstr "Bitte geben Sie eine gültige Domain ein."
#: src/Admin/OrderLicenseController.php:308 #: src/Admin/OrderLicenseController.php:308
#, fuzzy
msgid "Invalid order ID." msgid "Invalid order ID."
msgstr "Ungültige Lizenz-ID." msgstr "Ungültige Bestellungs-ID."
#: src/Admin/OrderLicenseController.php:319 #: src/Admin/OrderLicenseController.php:319
#: src/Admin/OrderLicenseController.php:357 #: src/Admin/OrderLicenseController.php:357
#, fuzzy
msgid "Invalid domain format." msgid "Invalid domain format."
msgstr "Ungültiges Datumsformat." msgstr "Ungültiges Domain-Format."
#: src/Admin/OrderLicenseController.php:327 #: src/Admin/OrderLicenseController.php:327
#, fuzzy
msgid "Order domain updated." msgid "Order domain updated."
msgstr "%d aktualisiert." msgstr "Bestellungs-Domain aktualisiert."
#: src/Admin/OrderLicenseController.php:363 #: src/Admin/OrderLicenseController.php:363
#: src/Frontend/AccountController.php:351 #: src/Frontend/AccountController.php:352
#: src/Frontend/DownloadController.php:105 #: src/Frontend/DownloadController.php:105
msgid "License not found." msgid "License not found."
msgstr "Lizenz nicht gefunden." msgstr "Lizenz nicht gefunden."
#: src/Admin/OrderLicenseController.php:371 #: src/Admin/OrderLicenseController.php:371
#, fuzzy
msgid "License domain updated." msgid "License domain updated."
msgstr "Lizenz-Domain" msgstr "Lizenz-Domain aktualisiert."
#: src/Admin/OrderLicenseController.php:375 #: src/Admin/OrderLicenseController.php:375
msgid "Failed to update license domain." msgid "Failed to update license domain."
@@ -820,7 +802,6 @@ msgstr ""
msgid "Expiration Warning Schedule" msgid "Expiration Warning Schedule"
msgstr "Ablaufwarnung Zeitplan" msgstr "Ablaufwarnung Zeitplan"
#. translators: %s: URL to WooCommerce email settings
#: src/Admin/SettingsController.php:101 #: src/Admin/SettingsController.php:101
#, php-format #, php-format
msgid "" msgid ""
@@ -863,7 +844,7 @@ msgid "Add New Version"
msgstr "Neue Version hinzufügen" msgstr "Neue Version hinzufügen"
#: src/Admin/VersionAdminController.php:81 #: src/Admin/VersionAdminController.php:81
#: src/Admin/VersionAdminController.php:129 #: src/Admin/VersionAdminController.php:136
msgid "Version" msgid "Version"
msgstr "Version" msgstr "Version"
@@ -872,7 +853,7 @@ msgid "Use semantic versioning (e.g., 1.0.0)"
msgstr "Verwenden Sie semantische Versionierung (z.B. 1.0.0)" msgstr "Verwenden Sie semantische Versionierung (z.B. 1.0.0)"
#: src/Admin/VersionAdminController.php:88 #: src/Admin/VersionAdminController.php:88
#: src/Admin/VersionAdminController.php:130 #: src/Admin/VersionAdminController.php:137
msgid "Download File" msgid "Download File"
msgstr "Download-Datei" msgstr "Download-Datei"
@@ -881,6 +862,7 @@ msgid "Select File"
msgstr "Datei auswählen" msgstr "Datei auswählen"
#: src/Admin/VersionAdminController.php:96 #: src/Admin/VersionAdminController.php:96
#: src/Admin/VersionAdminController.php:110
msgid "Remove" msgid "Remove"
msgstr "Entfernen" msgstr "Entfernen"
@@ -893,121 +875,138 @@ msgstr ""
"Version wird automatisch aus dem Dateinamen erkannt (z.B. plugin-v1.2.3.zip)." "Version wird automatisch aus dem Dateinamen erkannt (z.B. plugin-v1.2.3.zip)."
#: src/Admin/VersionAdminController.php:102 #: src/Admin/VersionAdminController.php:102
msgid "Or External URL" msgid "Checksum File"
msgstr "Oder externe URL" msgstr "Prüfsummendatei"
#: src/Admin/VersionAdminController.php:105 #: src/Admin/VersionAdminController.php:107
msgid "Select Checksum File"
msgstr "Prüfsummendatei auswählen"
#: src/Admin/VersionAdminController.php:112
msgid "" msgid ""
"Alternative: Enter an external download URL instead of uploading a file." "Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity."
msgstr "" msgstr ""
"Alternativ: Geben Sie eine externe Download-URL ein, anstatt eine Datei " "Laden Sie eine SHA256-Prüfsummendatei (.sha256 oder .txt) hoch, um die "
"hochzuladen." "Dateiintegrität zu überprüfen."
#: src/Admin/VersionAdminController.php:109 #: src/Admin/VersionAdminController.php:116
#: src/Admin/VersionAdminController.php:131 #: src/Admin/VersionAdminController.php:139
msgid "Release Notes" msgid "Release Notes"
msgstr "Versionshinweise" msgstr "Versionshinweise"
#: src/Admin/VersionAdminController.php:117 #: src/Admin/VersionAdminController.php:124
msgid "Add Version" msgid "Add Version"
msgstr "Version hinzufügen" msgstr "Version hinzufügen"
#: src/Admin/VersionAdminController.php:125 #: src/Admin/VersionAdminController.php:132
msgid "Existing Versions" msgid "Existing Versions"
msgstr "Vorhandene Versionen" msgstr "Vorhandene Versionen"
#: src/Admin/VersionAdminController.php:133 #: src/Admin/VersionAdminController.php:138
msgid "SHA256"
msgstr "SHA256"
#: src/Admin/VersionAdminController.php:141
msgid "Released" msgid "Released"
msgstr "Veröffentlicht" msgstr "Veröffentlicht"
#: src/Admin/VersionAdminController.php:140 #: src/Admin/VersionAdminController.php:148
msgid "No versions found. Add your first version above." msgid "No versions found. Add your first version above."
msgstr "Keine Versionen gefunden. Fügen Sie Ihre erste Version oben hinzu." msgstr "Keine Versionen gefunden. Fügen Sie Ihre erste Version oben hinzu."
#: src/Admin/VersionAdminController.php:156 #: src/Admin/VersionAdminController.php:164
#: src/Admin/VersionAdminController.php:372 #: src/Admin/VersionAdminController.php:393
msgid "Uploaded file" msgid "Uploaded file"
msgstr "Hochgeladene Datei" msgstr "Hochgeladene Datei"
#: src/Admin/VersionAdminController.php:159 #: src/Admin/VersionAdminController.php:167
#: src/Admin/VersionAdminController.php:375 #: src/Admin/VersionAdminController.php:396
msgid "No download file" msgid "No download file"
msgstr "Keine Download-Datei" msgstr "Keine Download-Datei"
#: src/Admin/VersionAdminController.php:215 #: src/Admin/VersionAdminController.php:230
msgid "Are you sure you want to delete this version?" msgid "Are you sure you want to delete this version?"
msgstr "Sind Sie sicher, dass Sie diese Version löschen möchten?" msgstr "Sind Sie sicher, dass Sie diese Version löschen möchten?"
#: src/Admin/VersionAdminController.php:216 #: src/Admin/VersionAdminController.php:231
msgid "Please enter a version number." msgid "Please enter a version number."
msgstr "Bitte geben Sie eine Versionsnummer ein." msgstr "Bitte geben Sie eine Versionsnummer ein."
#: src/Admin/VersionAdminController.php:217 #: src/Admin/VersionAdminController.php:232
msgid "Please enter a valid version number (e.g., 1.0.0)." msgid "Please enter a valid version number (e.g., 1.0.0)."
msgstr "Bitte geben Sie eine gültige Versionsnummer ein (z.B. 1.0.0)." msgstr "Bitte geben Sie eine gültige Versionsnummer ein (z.B. 1.0.0)."
#: src/Admin/VersionAdminController.php:218 #: src/Admin/VersionAdminController.php:233
msgid "An error occurred. Please try again." msgid "An error occurred. Please try again."
msgstr "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut." msgstr "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
#: src/Admin/VersionAdminController.php:219 #: src/Admin/VersionAdminController.php:234
msgid "Select Download File" msgid "Select Download File"
msgstr "Download-Datei auswählen" msgstr "Download-Datei auswählen"
#: src/Admin/VersionAdminController.php:220 #: src/Admin/VersionAdminController.php:235
msgid "Use this file" msgid "Use this file"
msgstr "Diese Datei verwenden" msgstr "Diese Datei verwenden"
#: src/Admin/VersionAdminController.php:250 #: src/Admin/VersionAdminController.php:236
msgid ""
"Invalid checksum file format. File must contain a 64-character SHA256 hash."
msgstr ""
"Ungültiges Prüfsummendateiformat. Die Datei muss einen 64-stelligen SHA256-"
"Hash enthalten."
#: src/Admin/VersionAdminController.php:237
msgid "Failed to read checksum file."
msgstr "Prüfsummendatei konnte nicht gelesen werden."
#: src/Admin/VersionAdminController.php:267
msgid "Product ID and version are required." msgid "Product ID and version are required."
msgstr "Produkt-ID und Version sind erforderlich." msgstr "Produkt-ID und Version sind erforderlich."
#: src/Admin/VersionAdminController.php:255 #: src/Admin/VersionAdminController.php:272
msgid "Invalid version format. Use semantic versioning (e.g., 1.0.0)." msgid "Invalid version format. Use semantic versioning (e.g., 1.0.0)."
msgstr "" msgstr ""
"Ungültiges Versionsformat. Verwenden Sie semantische Versionierung (z.B. " "Ungültiges Versionsformat. Verwenden Sie semantische Versionierung (z.B. "
"1.0.0)." "1.0.0)."
#: src/Admin/VersionAdminController.php:260 #: src/Admin/VersionAdminController.php:277
msgid "This version already exists." msgid "This version already exists."
msgstr "Diese Version existiert bereits." msgstr "Diese Version existiert bereits."
#: src/Admin/VersionAdminController.php:266
#, fuzzy
msgid "Product not found."
msgstr "Version nicht gefunden."
#: src/Admin/VersionAdminController.php:270
#, fuzzy
msgid "This product is not a licensed product."
msgstr "Version stimmt nicht mit Ihrem lizensierten Produkt überein."
#: src/Admin/VersionAdminController.php:283 #: src/Admin/VersionAdminController.php:283
msgid "Product not found."
msgstr "Produkt nicht gefunden."
#: src/Admin/VersionAdminController.php:287
msgid "This product is not a licensed product."
msgstr "Dieses Produkt ist kein lizensiertes Produkt."
#: src/Admin/VersionAdminController.php:304
msgid "Failed to create version." msgid "Failed to create version."
msgstr "Version konnte nicht erstellt werden." msgstr "Version konnte nicht erstellt werden."
#: src/Admin/VersionAdminController.php:291 #: src/Admin/VersionAdminController.php:312
msgid "Version added successfully." msgid "Version added successfully."
msgstr "Version erfolgreich hinzugefügt." msgstr "Version erfolgreich hinzugefügt."
#: src/Admin/VersionAdminController.php:311 #: src/Admin/VersionAdminController.php:332
#: src/Admin/VersionAdminController.php:338 #: src/Admin/VersionAdminController.php:359
msgid "Version ID is required." msgid "Version ID is required."
msgstr "Versions-ID ist erforderlich." msgstr "Versions-ID ist erforderlich."
#: src/Admin/VersionAdminController.php:317 #: src/Admin/VersionAdminController.php:338
msgid "Failed to delete version." msgid "Failed to delete version."
msgstr "Version konnte nicht gelöscht werden." msgstr "Version konnte nicht gelöscht werden."
#: src/Admin/VersionAdminController.php:320 #: src/Admin/VersionAdminController.php:341
msgid "Version deleted successfully." msgid "Version deleted successfully."
msgstr "Version erfolgreich gelöscht." msgstr "Version erfolgreich gelöscht."
#: src/Admin/VersionAdminController.php:344 #: src/Admin/VersionAdminController.php:365
msgid "Failed to update version." msgid "Failed to update version."
msgstr "Version konnte nicht aktualisiert werden." msgstr "Version konnte nicht aktualisiert werden."
#: src/Admin/VersionAdminController.php:348 #: src/Admin/VersionAdminController.php:369
msgid "Version updated successfully." msgid "Version updated successfully."
msgstr "Version erfolgreich aktualisiert." msgstr "Version erfolgreich aktualisiert."
@@ -1040,28 +1039,32 @@ msgstr "Lizenz konnte nicht aktiviert werden."
msgid "License activated successfully." msgid "License activated successfully."
msgstr "Lizenz erfolgreich aktiviert." msgstr "Lizenz erfolgreich aktiviert."
#: src/Checkout/CheckoutController.php:78
#: src/Checkout/CheckoutBlocksIntegration.php:104
msgid "License Domain"
msgstr "Lizenz-Domain"
#: src/Checkout/CheckoutController.php:81
#: src/Checkout/CheckoutBlocksIntegration.php:101 #: src/Checkout/CheckoutBlocksIntegration.php:101
#: src/Checkout/CheckoutController.php:81
msgid "Domain for License Activation" msgid "Domain for License Activation"
msgstr "Domain für Lizenz-Aktivierung" msgstr "Domain für Lizenz-Aktivierung"
#: src/Checkout/CheckoutController.php:82
msgid "required"
msgstr "erforderlich"
#: src/Checkout/CheckoutController.php:93
#: src/Checkout/CheckoutBlocksIntegration.php:103 #: src/Checkout/CheckoutBlocksIntegration.php:103
#: src/Checkout/CheckoutController.php:93
msgid "" msgid ""
"Enter the domain where you will use this license (without http:// or www)." "Enter the domain where you will use this license (without http:// or www)."
msgstr "" msgstr ""
"Geben Sie die Domain ein, auf der Sie diese Lizenz verwenden möchten (ohne " "Geben Sie die Domain ein, auf der Sie diese Lizenz verwenden möchten (ohne "
"http:// oder www)." "http:// oder www)."
#: src/Checkout/CheckoutBlocksIntegration.php:104
#: src/Checkout/CheckoutController.php:78
msgid "License Domain"
msgstr "Lizenz-Domain"
#: src/Checkout/CheckoutBlocksIntegration.php:105
msgid "Please enter a valid domain for your license activation."
msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz-Aktivierung ein."
#: src/Checkout/CheckoutController.php:82
msgid "required"
msgstr "erforderlich"
#: src/Checkout/CheckoutController.php:115 #: src/Checkout/CheckoutController.php:115
msgid "Please enter a domain for your license activation." msgid "Please enter a domain for your license activation."
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz-Aktivierung ein." msgstr "Bitte geben Sie eine Domain für Ihre Lizenz-Aktivierung ein."
@@ -1076,13 +1079,7 @@ msgstr "Bitte geben Sie einen gültigen Domain-Namen ein."
msgid "License Domain:" msgid "License Domain:"
msgstr "Lizenz-Domain:" msgstr "Lizenz-Domain:"
#: src/Checkout/CheckoutBlocksIntegration.php:105
#, fuzzy
msgid "Please enter a valid domain for your license activation."
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz-Aktivierung ein."
#: src/Checkout/StoreApiExtension.php:85 #: src/Checkout/StoreApiExtension.php:85
#, fuzzy
msgid "Domain for license activation" msgid "Domain for license activation"
msgstr "Domain für Lizenz-Aktivierung" msgstr "Domain für Lizenz-Aktivierung"
@@ -1091,7 +1088,7 @@ msgstr "Domain für Lizenz-Aktivierung"
#: src/Email/LicenseEmailController.php:281 #: src/Email/LicenseEmailController.php:281
#: src/Email/LicenseExpirationEmail.php:207 #: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseExpirationEmail.php:270
#: src/Frontend/AccountController.php:189 #: src/Frontend/AccountController.php:190
msgid "License Key:" msgid "License Key:"
msgstr "Lizenzschlüssel:" msgstr "Lizenzschlüssel:"
@@ -1106,7 +1103,7 @@ msgstr "Lizensierte Domain:"
#: src/Email/LicenseEmailController.php:248 #: src/Email/LicenseEmailController.php:248
#: src/Email/LicenseEmailController.php:287 #: src/Email/LicenseEmailController.php:287
#: src/Frontend/AccountController.php:217 #: src/Frontend/AccountController.php:218
msgid "Never" msgid "Never"
msgstr "Nie" msgstr "Nie"
@@ -1123,7 +1120,7 @@ msgstr "IHRE LIZENZSCHLÜSSEL"
#: src/Email/LicenseEmailController.php:284 #: src/Email/LicenseEmailController.php:284
#: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:219
#: src/Email/LicenseExpirationEmail.php:272 #: src/Email/LicenseExpirationEmail.php:272
#: src/Frontend/AccountController.php:212 #: src/Frontend/AccountController.php:213
msgid "Expires:" msgid "Expires:"
msgstr "Läuft ab:" msgstr "Läuft ab:"
@@ -1152,7 +1149,7 @@ msgid "License Expiration Notice"
msgstr "Lizenzablauf-Benachrichtigung" msgstr "Lizenzablauf-Benachrichtigung"
#: src/Email/LicenseExpirationEmail.php:107 #: src/Email/LicenseExpirationEmail.php:107
#: src/Frontend/AccountController.php:139 src/License/LicenseManager.php:760 #: src/Frontend/AccountController.php:140 src/License/LicenseManager.php:760
msgid "Unknown Product" msgid "Unknown Product"
msgstr "Unbekanntes Produkt" msgstr "Unbekanntes Produkt"
@@ -1186,7 +1183,7 @@ msgstr "Produkt:"
#: src/Email/LicenseExpirationEmail.php:215 #: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271 #: src/Email/LicenseExpirationEmail.php:271
#: src/Frontend/AccountController.php:200 #: src/Frontend/AccountController.php:201
msgid "Domain:" msgid "Domain:"
msgstr "Domain:" msgstr "Domain:"
@@ -1203,7 +1200,6 @@ msgstr ""
"Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor " "Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor "
"dem Ablaufdatum." "dem Ablaufdatum."
#. translators: %s: list of placeholders
#: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpirationEmail.php:301
#, php-format #, php-format
msgid "Available placeholders: %s" msgid "Available placeholders: %s"
@@ -1245,61 +1241,61 @@ msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll."
msgid "Please log in to view your licenses." msgid "Please log in to view your licenses."
msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen." msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
#: src/Frontend/AccountController.php:164 #: src/Frontend/AccountController.php:165
msgid "You have no licenses yet." msgid "You have no licenses yet."
msgstr "Sie haben noch keine Lizenzen." msgstr "Sie haben noch keine Lizenzen."
#: src/Frontend/AccountController.php:206 #: src/Frontend/AccountController.php:207
msgid "Transfer to new domain" msgid "Transfer to new domain"
msgstr "Auf neue Domain übertragen" msgstr "Auf neue Domain übertragen"
#: src/Frontend/AccountController.php:208 #: src/Frontend/AccountController.php:209
msgid "Transfer" msgid "Transfer"
msgstr "Übertragen" msgstr "Übertragen"
#: src/Frontend/AccountController.php:225 #: src/Frontend/AccountController.php:226
msgid "Available Downloads" msgid "Available Downloads"
msgstr "Verfügbare Downloads" msgstr "Verfügbare Downloads"
#: src/Frontend/AccountController.php:231 #: src/Frontend/AccountController.php:232
#, php-format #, php-format
msgid "Version %s" msgid "Version %s"
msgstr "Version %s" msgstr "Version %s"
#: src/Frontend/AccountController.php:248 #: src/Frontend/AccountController.php:249
msgid "Close" msgid "Close"
msgstr "Schliessen" msgstr "Schliessen"
#: src/Frontend/AccountController.php:249 #: src/Frontend/AccountController.php:250
msgid "Transfer License to New Domain" msgid "Transfer License to New Domain"
msgstr "Lizenz auf neue Domain übertragen" msgstr "Lizenz auf neue Domain übertragen"
#: src/Frontend/AccountController.php:254 #: src/Frontend/AccountController.php:255
msgid "Current Domain" msgid "Current Domain"
msgstr "Aktuelle Domain" msgstr "Aktuelle Domain"
#: src/Frontend/AccountController.php:259 #: src/Frontend/AccountController.php:260
msgid "New Domain" msgid "New Domain"
msgstr "Neue Domain" msgstr "Neue Domain"
#: src/Frontend/AccountController.php:263 #: src/Frontend/AccountController.php:264
msgid "Enter the new domain without http:// or www." msgid "Enter the new domain without http:// or www."
msgstr "Geben Sie die neue Domain ohne http:// oder www ein." msgstr "Geben Sie die neue Domain ohne http:// oder www ein."
#: src/Frontend/AccountController.php:268 #: src/Frontend/AccountController.php:269
msgid "Transfer License" msgid "Transfer License"
msgstr "Lizenz übertragen" msgstr "Lizenz übertragen"
#: src/Frontend/AccountController.php:310 #: src/Frontend/AccountController.php:311
#: src/Frontend/AccountController.php:377 #: src/Frontend/AccountController.php:378
msgid "License transferred successfully!" msgid "License transferred successfully!"
msgstr "Lizenz erfolgreich übertragen!" msgstr "Lizenz erfolgreich übertragen!"
#: src/Frontend/AccountController.php:311 #: src/Frontend/AccountController.php:312
msgid "Transfer failed. Please try again." msgid "Transfer failed. Please try again."
msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut." msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
#: src/Frontend/AccountController.php:312 #: src/Frontend/AccountController.php:313
msgid "" msgid ""
"Are you sure you want to transfer this license to a new domain? This action " "Are you sure you want to transfer this license to a new domain? This action "
"cannot be undone." "cannot be undone."
@@ -1307,34 +1303,33 @@ msgstr ""
"Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen " "Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen "
"möchten? Diese Aktion kann nicht rückgängig gemacht werden." "möchten? Diese Aktion kann nicht rückgängig gemacht werden."
#: src/Frontend/AccountController.php:331 #: src/Frontend/AccountController.php:332
msgid "Please log in to transfer a license." msgid "Please log in to transfer a license."
msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen." msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen."
#: src/Frontend/AccountController.php:337 #: src/Frontend/AccountController.php:338
msgid "Invalid license." msgid "Invalid license."
msgstr "Ungültige Lizenz." msgstr "Ungültige Lizenz."
#: src/Frontend/AccountController.php:355 #: src/Frontend/AccountController.php:356
msgid "You do not have permission to transfer this license." msgid "You do not have permission to transfer this license."
msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen." msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen."
#: src/Frontend/AccountController.php:360 #: src/Frontend/AccountController.php:361
msgid "Revoked licenses cannot be transferred." msgid "Revoked licenses cannot be transferred."
msgstr "Widerrufene Lizenzen können nicht übertragen werden." msgstr "Widerrufene Lizenzen können nicht übertragen werden."
#: src/Frontend/AccountController.php:364 #: src/Frontend/AccountController.php:365
msgid "Expired licenses cannot be transferred." msgid "Expired licenses cannot be transferred."
msgstr "Abgelaufene Lizenzen können nicht übertragen werden." msgstr "Abgelaufene Lizenzen können nicht übertragen werden."
#: src/Frontend/AccountController.php:369 #: src/Frontend/AccountController.php:370
msgid "The new domain is the same as the current domain." msgid "The new domain is the same as the current domain."
msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain." msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain."
#: src/Frontend/AccountController.php:381 #: src/Frontend/AccountController.php:382
#, fuzzy
msgid "Failed to transfer license. Please try again." msgid "Failed to transfer license. Please try again."
msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut." msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
#: src/Frontend/DownloadController.php:65 #: src/Frontend/DownloadController.php:65
#: src/Frontend/DownloadController.php:89 #: src/Frontend/DownloadController.php:89
@@ -1416,7 +1411,6 @@ msgstr "Lizenz-Einstellungen"
msgid "%d days" msgid "%d days"
msgstr "%d Tage" msgstr "%d Tage"
#. translators: %s: URL to settings page
#: src/Product/LicensedProductType.php:113 #: src/Product/LicensedProductType.php:113
#, php-format #, php-format
msgid "Leave fields empty to use default settings from %s." msgid "Leave fields empty to use default settings from %s."
@@ -1430,7 +1424,6 @@ msgstr "WooCommerce > Einstellungen > Lizensierte Produkte"
msgid "Max Activations" msgid "Max Activations"
msgstr "Max. Aktivierungen" msgstr "Max. Aktivierungen"
#. translators: %d: default max activations value
#: src/Product/LicensedProductType.php:125 #: src/Product/LicensedProductType.php:125
#, php-format #, php-format
msgid "Maximum number of domain activations per license. Default: %d" msgid "Maximum number of domain activations per license. Default: %d"
@@ -1440,7 +1433,6 @@ msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d"
msgid "License Validity (Days)" msgid "License Validity (Days)"
msgstr "Lizenz-Gültigkeit (Tage)" msgstr "Lizenz-Gültigkeit (Tage)"
#. translators: %s: default validity value
#: src/Product/LicensedProductType.php:143 #: src/Product/LicensedProductType.php:143
#, php-format #, php-format
msgid "Number of days the license is valid. Leave empty for default (%s)." msgid "Number of days the license is valid. Leave empty for default (%s)."
@@ -1450,7 +1442,6 @@ msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s).
msgid "Bind to Major Version" msgid "Bind to Major Version"
msgstr "An Hauptversion binden" msgstr "An Hauptversion binden"
#. translators: %s: default bind to version value (Yes/No)
#: src/Product/LicensedProductType.php:161 #: src/Product/LicensedProductType.php:161
#, php-format #, php-format
msgid "" msgid ""
@@ -1468,6 +1459,34 @@ msgstr "Ja"
msgid "No" msgid "No"
msgstr "Nein" msgstr "Nein"
#: 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"
#~ msgid "Or External URL"
#~ msgstr "Oder externe URL"
#~ msgid ""
#~ "Alternative: Enter an external download URL instead of uploading a file."
#~ msgstr ""
#~ "Alternativ: Geben Sie eine externe Download-URL ein, anstatt eine Datei "
#~ "hochzuladen."
#~ msgid "Enter SHA256 checksum..."
#~ msgstr "SHA256 Prüfsumme eingeben..."
#~ msgid ""
#~ "SHA256 checksum of the uploaded file (optional but recommended for "
#~ "integrity verification)."
#~ msgstr ""
#~ "SHA256 Prüfsumme der hochgeladenen Datei (optional, aber empfohlen zur "
#~ "Integritätsprüfung)."
#~ msgid "Maximum number of domain activations per license. Default: 1" #~ msgid "Maximum number of domain activations per license. Default: 1"
#~ msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: 1" #~ msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: 1"

View File

@@ -6,9 +6,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WooCommerce Licensed Product 0.1.0\n" "Project-Id-Version: WooCommerce Licensed Product 0.2.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-22 11:52+0100\n" "POT-Creation-Date: 2026-01-22 17:32+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"
@@ -18,7 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#. translators: %s: WooCommerce plugin name
#: wc-licensed-product.php:61 #: wc-licensed-product.php:61
#, php-format #, php-format
msgid "%s requires WooCommerce to be installed and active." msgid "%s requires WooCommerce to be installed and active."
@@ -81,7 +80,7 @@ msgstr ""
#: src/Admin/AdminController.php:146 src/Admin/AdminController.php:1303 #: src/Admin/AdminController.php:146 src/Admin/AdminController.php:1303
#: src/Admin/AdminController.php:1323 src/Admin/AdminController.php:1344 #: src/Admin/AdminController.php:1323 src/Admin/AdminController.php:1344
#: src/Admin/OrderLicenseController.php:185 #: src/Admin/OrderLicenseController.php:185
#: src/Frontend/AccountController.php:270 #: src/Frontend/AccountController.php:271
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@@ -99,25 +98,25 @@ msgstr ""
msgid "Lifetime" msgid "Lifetime"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:149 src/Frontend/AccountController.php:308 #: src/Admin/AdminController.php:149 src/Frontend/AccountController.php:309
msgid "Copied!" msgid "Copied!"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:150 src/Frontend/AccountController.php:309 #: src/Admin/AdminController.php:150 src/Frontend/AccountController.php:310
msgid "Copy failed" msgid "Copy failed"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:153 src/Admin/AdminController.php:875 #: src/Admin/AdminController.php:153 src/Admin/AdminController.php:875
#: src/Admin/AdminController.php:1194 src/Admin/AdminController.php:1317 #: src/Admin/AdminController.php:1194 src/Admin/AdminController.php:1317
#: src/Admin/VersionAdminController.php:165 #: src/Admin/VersionAdminController.php:180
#: src/Admin/VersionAdminController.php:381 #: src/Admin/VersionAdminController.php:409
msgid "Active" msgid "Active"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:154 src/Admin/AdminController.php:882 #: src/Admin/AdminController.php:154 src/Admin/AdminController.php:882
#: src/Admin/AdminController.php:1195 src/Admin/AdminController.php:1318 #: src/Admin/AdminController.php:1195 src/Admin/AdminController.php:1318
#: src/Admin/VersionAdminController.php:165 #: src/Admin/VersionAdminController.php:180
#: src/Admin/VersionAdminController.php:381 #: src/Admin/VersionAdminController.php:409
msgid "Inactive" msgid "Inactive"
msgstr "" msgstr ""
@@ -135,9 +134,9 @@ msgstr ""
#: src/Admin/AdminController.php:246 src/Admin/AdminController.php:298 #: src/Admin/AdminController.php:246 src/Admin/AdminController.php:298
#: src/Admin/AdminController.php:336 src/Admin/OrderLicenseController.php:301 #: src/Admin/AdminController.php:336 src/Admin/OrderLicenseController.php:301
#: src/Admin/OrderLicenseController.php:340 #: src/Admin/OrderLicenseController.php:340
#: src/Admin/VersionAdminController.php:240 #: src/Admin/VersionAdminController.php:257
#: src/Admin/VersionAdminController.php:305 #: src/Admin/VersionAdminController.php:326
#: src/Admin/VersionAdminController.php:331 #: src/Admin/VersionAdminController.php:352
msgid "Permission denied." msgid "Permission denied."
msgstr "" msgstr ""
@@ -208,7 +207,7 @@ msgstr ""
#: src/Admin/AdminController.php:466 src/Admin/AdminController.php:484 #: src/Admin/AdminController.php:466 src/Admin/AdminController.php:484
#: src/Admin/AdminController.php:504 src/Admin/AdminController.php:522 #: src/Admin/AdminController.php:504 src/Admin/AdminController.php:522
#: src/Admin/AdminController.php:589 src/Admin/AdminController.php:779 #: src/Admin/AdminController.php:589 src/Admin/AdminController.php:779
#: src/Frontend/AccountController.php:325 #: src/Frontend/AccountController.php:326
msgid "Security check failed." msgid "Security check failed."
msgstr "" msgstr ""
@@ -284,7 +283,6 @@ msgstr ""
msgid "License set to lifetime successfully." msgid "License set to lifetime successfully."
msgstr "" msgstr ""
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1068 #: src/Admin/AdminController.php:1068
#, php-format #, php-format
msgid "%d license activated." msgid "%d license activated."
@@ -292,7 +290,6 @@ msgid_plural "%d licenses activated."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1076 #: src/Admin/AdminController.php:1076
#, php-format #, php-format
msgid "%d license deactivated." msgid "%d license deactivated."
@@ -300,7 +297,6 @@ msgid_plural "%d licenses deactivated."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1084 #: src/Admin/AdminController.php:1084
#, php-format #, php-format
msgid "%d license revoked." msgid "%d license revoked."
@@ -308,7 +304,6 @@ msgid_plural "%d licenses revoked."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1092 #: src/Admin/AdminController.php:1092
#, php-format #, php-format
msgid "%d license deleted." msgid "%d license deleted."
@@ -316,7 +311,6 @@ msgid_plural "%d licenses deleted."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses
#: src/Admin/AdminController.php:1100 #: src/Admin/AdminController.php:1100
#, php-format #, php-format
msgid "%d license extended." msgid "%d license extended."
@@ -336,7 +330,6 @@ msgstr ""
msgid "No licenses to export." msgid "No licenses to export."
msgstr "" msgstr ""
#. translators: %d: number of licenses imported
#: src/Admin/AdminController.php:1121 #: src/Admin/AdminController.php:1121
#, php-format #, php-format
msgid "%d license imported." msgid "%d license imported."
@@ -344,7 +337,6 @@ msgid_plural "%d licenses imported."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses updated
#: src/Admin/AdminController.php:1128 #: src/Admin/AdminController.php:1128
#, php-format #, php-format
msgid "%d updated." msgid "%d updated."
@@ -352,7 +344,6 @@ msgid_plural "%d updated."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of licenses skipped
#: src/Admin/AdminController.php:1136 #: src/Admin/AdminController.php:1136
#, php-format #, php-format
msgid "%d skipped." msgid "%d skipped."
@@ -360,7 +351,6 @@ msgid_plural "%d skipped."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. translators: %d: number of errors
#: src/Admin/AdminController.php:1144 #: src/Admin/AdminController.php:1144
#, php-format #, php-format
msgid "%d error." msgid "%d error."
@@ -437,14 +427,14 @@ msgid "Bulk Actions"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1407 #: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1407
#: src/Admin/VersionAdminController.php:171 #: src/Admin/VersionAdminController.php:186
#: src/Admin/VersionAdminController.php:387 #: src/Admin/VersionAdminController.php:415
msgid "Activate" msgid "Activate"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1236 src/Admin/AdminController.php:1408 #: src/Admin/AdminController.php:1236 src/Admin/AdminController.php:1408
#: src/Admin/VersionAdminController.php:171 #: src/Admin/VersionAdminController.php:186
#: src/Admin/VersionAdminController.php:387 #: src/Admin/VersionAdminController.php:415
msgid "Deactivate" msgid "Deactivate"
msgstr "" msgstr ""
@@ -466,8 +456,8 @@ msgid "Extend 1 year"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1241 src/Admin/AdminController.php:1377 #: src/Admin/AdminController.php:1241 src/Admin/AdminController.php:1377
#: src/Admin/AdminController.php:1413 src/Admin/VersionAdminController.php:174 #: src/Admin/AdminController.php:1413 src/Admin/VersionAdminController.php:189
#: src/Admin/VersionAdminController.php:390 #: src/Admin/VersionAdminController.php:418
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@@ -499,7 +489,7 @@ msgstr ""
#: src/Admin/AdminController.php:1257 src/Admin/AdminController.php:1395 #: src/Admin/AdminController.php:1257 src/Admin/AdminController.php:1395
#: src/Admin/OrderLicenseController.php:147 #: src/Admin/OrderLicenseController.php:147
#: src/Admin/VersionAdminController.php:132 #: src/Admin/VersionAdminController.php:140
msgid "Status" msgid "Status"
msgstr "" msgstr ""
@@ -515,7 +505,7 @@ msgstr ""
#: src/Admin/AdminController.php:1260 src/Admin/AdminController.php:1398 #: src/Admin/AdminController.php:1260 src/Admin/AdminController.php:1398
#: src/Admin/OrderLicenseController.php:149 #: src/Admin/OrderLicenseController.php:149
#: src/Admin/VersionAdminController.php:134 #: src/Admin/VersionAdminController.php:142
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -523,7 +513,7 @@ msgstr ""
msgid "No licenses found." msgid "No licenses found."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1276 src/Frontend/AccountController.php:193 #: src/Admin/AdminController.php:1276 src/Frontend/AccountController.php:194
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
@@ -670,8 +660,8 @@ msgid ""
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:116 #: src/Admin/OrderLicenseController.php:116
#: src/Checkout/CheckoutController.php:89
#: src/Checkout/CheckoutBlocksIntegration.php:102 #: src/Checkout/CheckoutBlocksIntegration.php:102
#: src/Checkout/CheckoutController.php:89
msgid "example.com" msgid "example.com"
msgstr "" msgstr ""
@@ -697,7 +687,6 @@ msgstr ""
msgid "View in Licenses" msgid "View in Licenses"
msgstr "" msgstr ""
#. translators: %s: Link to licenses page
#: src/Admin/OrderLicenseController.php:221 #: src/Admin/OrderLicenseController.php:221
#, php-format #, php-format
msgid "For more actions (revoke, extend, delete), go to the %s page." msgid "For more actions (revoke, extend, delete), go to the %s page."
@@ -712,8 +701,8 @@ msgid "Error saving. Please try again."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:288 #: src/Admin/OrderLicenseController.php:288
#: src/Frontend/AccountController.php:313 #: src/Frontend/AccountController.php:314
#: src/Frontend/AccountController.php:345 #: src/Frontend/AccountController.php:346
msgid "Please enter a valid domain." msgid "Please enter a valid domain."
msgstr "" msgstr ""
@@ -731,7 +720,7 @@ msgid "Order domain updated."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:363 #: src/Admin/OrderLicenseController.php:363
#: src/Frontend/AccountController.php:351 #: src/Frontend/AccountController.php:352
#: src/Frontend/DownloadController.php:105 #: src/Frontend/DownloadController.php:105
msgid "License not found." msgid "License not found."
msgstr "" msgstr ""
@@ -790,7 +779,6 @@ msgstr ""
msgid "Expiration Warning Schedule" msgid "Expiration Warning Schedule"
msgstr "" msgstr ""
#. translators: %s: URL to WooCommerce email settings
#: src/Admin/SettingsController.php:101 #: src/Admin/SettingsController.php:101
#, php-format #, php-format
msgid "" msgid ""
@@ -828,7 +816,7 @@ msgid "Add New Version"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:81 #: src/Admin/VersionAdminController.php:81
#: src/Admin/VersionAdminController.php:129 #: src/Admin/VersionAdminController.php:136
msgid "Version" msgid "Version"
msgstr "" msgstr ""
@@ -837,7 +825,7 @@ msgid "Use semantic versioning (e.g., 1.0.0)"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:88 #: src/Admin/VersionAdminController.php:88
#: src/Admin/VersionAdminController.php:130 #: src/Admin/VersionAdminController.php:137
msgid "Download File" msgid "Download File"
msgstr "" msgstr ""
@@ -846,6 +834,7 @@ msgid "Select File"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:96 #: src/Admin/VersionAdminController.php:96
#: src/Admin/VersionAdminController.php:110
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
@@ -856,115 +845,132 @@ msgid ""
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:102 #: src/Admin/VersionAdminController.php:102
msgid "Or External URL" msgid "Checksum File"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:105 #: src/Admin/VersionAdminController.php:107
msgid "Select Checksum File"
msgstr ""
#: src/Admin/VersionAdminController.php:112
msgid "" msgid ""
"Alternative: Enter an external download URL instead of uploading a file." "Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:109 #: src/Admin/VersionAdminController.php:116
#: src/Admin/VersionAdminController.php:131 #: src/Admin/VersionAdminController.php:139
msgid "Release Notes" msgid "Release Notes"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:117 #: src/Admin/VersionAdminController.php:124
msgid "Add Version" msgid "Add Version"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:125 #: src/Admin/VersionAdminController.php:132
msgid "Existing Versions" msgid "Existing Versions"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:133 #: src/Admin/VersionAdminController.php:138
msgid "SHA256"
msgstr ""
#: src/Admin/VersionAdminController.php:141
msgid "Released" msgid "Released"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:140 #: src/Admin/VersionAdminController.php:148
msgid "No versions found. Add your first version above." msgid "No versions found. Add your first version above."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:156 #: src/Admin/VersionAdminController.php:164
#: src/Admin/VersionAdminController.php:372 #: src/Admin/VersionAdminController.php:393
msgid "Uploaded file" msgid "Uploaded file"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:159 #: src/Admin/VersionAdminController.php:167
#: src/Admin/VersionAdminController.php:375 #: src/Admin/VersionAdminController.php:396
msgid "No download file" msgid "No download file"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:215 #: src/Admin/VersionAdminController.php:230
msgid "Are you sure you want to delete this version?" msgid "Are you sure you want to delete this version?"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:216 #: src/Admin/VersionAdminController.php:231
msgid "Please enter a version number." msgid "Please enter a version number."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:217 #: src/Admin/VersionAdminController.php:232
msgid "Please enter a valid version number (e.g., 1.0.0)." msgid "Please enter a valid version number (e.g., 1.0.0)."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:218 #: src/Admin/VersionAdminController.php:233
msgid "An error occurred. Please try again." msgid "An error occurred. Please try again."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:219 #: src/Admin/VersionAdminController.php:234
msgid "Select Download File" msgid "Select Download File"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:220 #: src/Admin/VersionAdminController.php:235
msgid "Use this file" msgid "Use this file"
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:250 #: src/Admin/VersionAdminController.php:236
msgid ""
"Invalid checksum file format. File must contain a 64-character SHA256 hash."
msgstr ""
#: src/Admin/VersionAdminController.php:237
msgid "Failed to read checksum file."
msgstr ""
#: src/Admin/VersionAdminController.php:267
msgid "Product ID and version are required." msgid "Product ID and version are required."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:255 #: src/Admin/VersionAdminController.php:272
msgid "Invalid version format. Use semantic versioning (e.g., 1.0.0)." msgid "Invalid version format. Use semantic versioning (e.g., 1.0.0)."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:260 #: src/Admin/VersionAdminController.php:277
msgid "This version already exists." msgid "This version already exists."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:266 #: src/Admin/VersionAdminController.php:283
msgid "Product not found." msgid "Product not found."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:270 #: src/Admin/VersionAdminController.php:287
msgid "This product is not a licensed product." msgid "This product is not a licensed product."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:283 #: src/Admin/VersionAdminController.php:304
msgid "Failed to create version." msgid "Failed to create version."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:291 #: src/Admin/VersionAdminController.php:312
msgid "Version added successfully." msgid "Version added successfully."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:311 #: src/Admin/VersionAdminController.php:332
#: src/Admin/VersionAdminController.php:338 #: src/Admin/VersionAdminController.php:359
msgid "Version ID is required." msgid "Version ID is required."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:317 #: src/Admin/VersionAdminController.php:338
msgid "Failed to delete version." msgid "Failed to delete version."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:320 #: src/Admin/VersionAdminController.php:341
msgid "Version deleted successfully." msgid "Version deleted successfully."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:344 #: src/Admin/VersionAdminController.php:365
msgid "Failed to update version." msgid "Failed to update version."
msgstr "" msgstr ""
#: src/Admin/VersionAdminController.php:348 #: src/Admin/VersionAdminController.php:369
msgid "Version updated successfully." msgid "Version updated successfully."
msgstr "" msgstr ""
@@ -997,26 +1003,30 @@ msgstr ""
msgid "License activated successfully." msgid "License activated successfully."
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:78 #: src/Checkout/CheckoutBlocksIntegration.php:101
#: src/Checkout/CheckoutController.php:81
msgid "Domain for License Activation"
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:103
#: src/Checkout/CheckoutController.php:93
msgid ""
"Enter the domain where you will use this license (without http:// or www)."
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:104 #: src/Checkout/CheckoutBlocksIntegration.php:104
#: src/Checkout/CheckoutController.php:78
msgid "License Domain" msgid "License Domain"
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:81 #: src/Checkout/CheckoutBlocksIntegration.php:105
#: src/Checkout/CheckoutBlocksIntegration.php:101 msgid "Please enter a valid domain for your license activation."
msgid "Domain for License Activation"
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:82 #: src/Checkout/CheckoutController.php:82
msgid "required" msgid "required"
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:93
#: src/Checkout/CheckoutBlocksIntegration.php:103
msgid ""
"Enter the domain where you will use this license (without http:// or www)."
msgstr ""
#: src/Checkout/CheckoutController.php:115 #: src/Checkout/CheckoutController.php:115
msgid "Please enter a domain for your license activation." msgid "Please enter a domain for your license activation."
msgstr "" msgstr ""
@@ -1031,10 +1041,6 @@ msgstr ""
msgid "License Domain:" msgid "License Domain:"
msgstr "" msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:105
msgid "Please enter a valid domain for your license activation."
msgstr ""
#: src/Checkout/StoreApiExtension.php:85 #: src/Checkout/StoreApiExtension.php:85
msgid "Domain for license activation" msgid "Domain for license activation"
msgstr "" msgstr ""
@@ -1044,7 +1050,7 @@ msgstr ""
#: src/Email/LicenseEmailController.php:281 #: src/Email/LicenseEmailController.php:281
#: src/Email/LicenseExpirationEmail.php:207 #: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseExpirationEmail.php:270
#: src/Frontend/AccountController.php:189 #: src/Frontend/AccountController.php:190
msgid "License Key:" msgid "License Key:"
msgstr "" msgstr ""
@@ -1059,7 +1065,7 @@ msgstr ""
#: src/Email/LicenseEmailController.php:248 #: src/Email/LicenseEmailController.php:248
#: src/Email/LicenseEmailController.php:287 #: src/Email/LicenseEmailController.php:287
#: src/Frontend/AccountController.php:217 #: src/Frontend/AccountController.php:218
msgid "Never" msgid "Never"
msgstr "" msgstr ""
@@ -1075,7 +1081,7 @@ msgstr ""
#: src/Email/LicenseEmailController.php:284 #: src/Email/LicenseEmailController.php:284
#: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:219
#: src/Email/LicenseExpirationEmail.php:272 #: src/Email/LicenseExpirationEmail.php:272
#: src/Frontend/AccountController.php:212 #: src/Frontend/AccountController.php:213
msgid "Expires:" msgid "Expires:"
msgstr "" msgstr ""
@@ -1100,7 +1106,7 @@ msgid "License Expiration Notice"
msgstr "" msgstr ""
#: src/Email/LicenseExpirationEmail.php:107 #: src/Email/LicenseExpirationEmail.php:107
#: src/Frontend/AccountController.php:139 src/License/LicenseManager.php:760 #: src/Frontend/AccountController.php:140 src/License/LicenseManager.php:760
msgid "Unknown Product" msgid "Unknown Product"
msgstr "" msgstr ""
@@ -1134,7 +1140,7 @@ msgstr ""
#: src/Email/LicenseExpirationEmail.php:215 #: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271 #: src/Email/LicenseExpirationEmail.php:271
#: src/Frontend/AccountController.php:200 #: src/Frontend/AccountController.php:201
msgid "Domain:" msgid "Domain:"
msgstr "" msgstr ""
@@ -1149,7 +1155,6 @@ msgid ""
"expiration date." "expiration date."
msgstr "" msgstr ""
#. translators: %s: list of placeholders
#: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpirationEmail.php:301
#, php-format #, php-format
msgid "Available placeholders: %s" msgid "Available placeholders: %s"
@@ -1191,91 +1196,91 @@ msgstr ""
msgid "Please log in to view your licenses." msgid "Please log in to view your licenses."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:164 #: src/Frontend/AccountController.php:165
msgid "You have no licenses yet." msgid "You have no licenses yet."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:206 #: src/Frontend/AccountController.php:207
msgid "Transfer to new domain" msgid "Transfer to new domain"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:208 #: src/Frontend/AccountController.php:209
msgid "Transfer" msgid "Transfer"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:225 #: src/Frontend/AccountController.php:226
msgid "Available Downloads" msgid "Available Downloads"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:231 #: src/Frontend/AccountController.php:232
#, php-format #, php-format
msgid "Version %s" msgid "Version %s"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:248 #: src/Frontend/AccountController.php:249
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:249 #: src/Frontend/AccountController.php:250
msgid "Transfer License to New Domain" msgid "Transfer License to New Domain"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:254 #: src/Frontend/AccountController.php:255
msgid "Current Domain" msgid "Current Domain"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:259 #: src/Frontend/AccountController.php:260
msgid "New Domain" msgid "New Domain"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:263 #: src/Frontend/AccountController.php:264
msgid "Enter the new domain without http:// or www." msgid "Enter the new domain without http:// or www."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:268 #: src/Frontend/AccountController.php:269
msgid "Transfer License" msgid "Transfer License"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:310 #: src/Frontend/AccountController.php:311
#: src/Frontend/AccountController.php:377 #: src/Frontend/AccountController.php:378
msgid "License transferred successfully!" msgid "License transferred successfully!"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:311 #: src/Frontend/AccountController.php:312
msgid "Transfer failed. Please try again." msgid "Transfer failed. Please try again."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:312 #: src/Frontend/AccountController.php:313
msgid "" msgid ""
"Are you sure you want to transfer this license to a new domain? This action " "Are you sure you want to transfer this license to a new domain? This action "
"cannot be undone." "cannot be undone."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:331 #: src/Frontend/AccountController.php:332
msgid "Please log in to transfer a license." msgid "Please log in to transfer a license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:337 #: src/Frontend/AccountController.php:338
msgid "Invalid license." msgid "Invalid license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:355 #: src/Frontend/AccountController.php:356
msgid "You do not have permission to transfer this license." msgid "You do not have permission to transfer this license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:360 #: src/Frontend/AccountController.php:361
msgid "Revoked licenses cannot be transferred." msgid "Revoked licenses cannot be transferred."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:364 #: src/Frontend/AccountController.php:365
msgid "Expired licenses cannot be transferred." msgid "Expired licenses cannot be transferred."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:369 #: src/Frontend/AccountController.php:370
msgid "The new domain is the same as the current domain." msgid "The new domain is the same as the current domain."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:381 #: src/Frontend/AccountController.php:382
msgid "Failed to transfer license. Please try again." msgid "Failed to transfer license. Please try again."
msgstr "" msgstr ""
@@ -1359,7 +1364,6 @@ msgstr ""
msgid "%d days" msgid "%d days"
msgstr "" msgstr ""
#. translators: %s: URL to settings page
#: src/Product/LicensedProductType.php:113 #: src/Product/LicensedProductType.php:113
#, php-format #, php-format
msgid "Leave fields empty to use default settings from %s." msgid "Leave fields empty to use default settings from %s."
@@ -1373,7 +1377,6 @@ msgstr ""
msgid "Max Activations" msgid "Max Activations"
msgstr "" msgstr ""
#. translators: %d: default max activations value
#: src/Product/LicensedProductType.php:125 #: src/Product/LicensedProductType.php:125
#, php-format #, php-format
msgid "Maximum number of domain activations per license. Default: %d" msgid "Maximum number of domain activations per license. Default: %d"
@@ -1383,7 +1386,6 @@ msgstr ""
msgid "License Validity (Days)" msgid "License Validity (Days)"
msgstr "" msgstr ""
#. translators: %s: default validity value
#: src/Product/LicensedProductType.php:143 #: src/Product/LicensedProductType.php:143
#, php-format #, php-format
msgid "Number of days the license is valid. Leave empty for default (%s)." msgid "Number of days the license is valid. Leave empty for default (%s)."
@@ -1393,7 +1395,6 @@ msgstr ""
msgid "Bind to Major Version" msgid "Bind to Major Version"
msgstr "" msgstr ""
#. translators: %s: default bind to version value (Yes/No)
#: src/Product/LicensedProductType.php:161 #: src/Product/LicensedProductType.php:161
#, php-format #, php-format
msgid "" msgid ""
@@ -1408,3 +1409,12 @@ msgstr ""
#: src/Product/LicensedProductType.php:162 #: src/Product/LicensedProductType.php:162
msgid "No" msgid "No"
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 ""

View File

@@ -0,0 +1 @@
20d90f61721b4579cb979cd19b0262f3286c3510dcb0345fe5e8da2703e3836f wc-licensed-product-0.2.0.zip

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
7b895090538f9063fac1509b6f7a40a2b71dc9958b3a255cbfcc60d0320ae5e5 releases/wc-licensed-product-0.2.1.zip

View File

@@ -98,11 +98,18 @@ final class VersionAdminController
<p class="description"><?php esc_html_e('Upload or select a file from the media library. Version will be auto-detected from filename (e.g., plugin-v1.2.3.zip).', 'wc-licensed-product'); ?></p> <p class="description"><?php esc_html_e('Upload or select a file from the media library. Version will be auto-detected from filename (e.g., plugin-v1.2.3.zip).', 'wc-licensed-product'); ?></p>
</td> </td>
</tr> </tr>
<tr> <tr id="sha256-hash-row" style="display: none;">
<th><label for="new_download_url"><?php esc_html_e('Or External URL', 'wc-licensed-product'); ?></label></th> <th><label for="new_checksum_file"><?php esc_html_e('Checksum File', 'wc-licensed-product'); ?></label></th>
<td> <td>
<input type="url" id="new_download_url" name="new_download_url" class="large-text" placeholder="https://" /> <input type="file" id="new_checksum_file" name="new_checksum_file" accept=".sha256,.txt" style="display: none;" />
<p class="description"><?php esc_html_e('Alternative: Enter an external download URL instead of uploading a file.', 'wc-licensed-product'); ?></p> <span id="selected_checksum_name" class="selected-file-name"></span>
<button type="button" class="button" id="select-checksum-file-btn">
<?php esc_html_e('Select Checksum File', 'wc-licensed-product'); ?>
</button>
<button type="button" class="button" id="remove-checksum-file-btn" style="display: none;">
<?php esc_html_e('Remove', 'wc-licensed-product'); ?>
</button>
<p class="description"><?php esc_html_e('Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity.', 'wc-licensed-product'); ?></p>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -128,6 +135,7 @@ final class VersionAdminController
<tr> <tr>
<th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th> <th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th> <th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('SHA256', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th> <th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th> <th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th> <th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th>
@@ -137,7 +145,7 @@ final class VersionAdminController
<tbody> <tbody>
<?php if (empty($versions)): ?> <?php if (empty($versions)): ?>
<tr class="no-versions"> <tr class="no-versions">
<td colspan="6"><?php esc_html_e('No versions found. Add your first version above.', 'wc-licensed-product'); ?></td> <td colspan="7"><?php esc_html_e('No versions found. Add your first version above.', 'wc-licensed-product'); ?></td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<?php foreach ($versions as $version): ?> <?php foreach ($versions as $version): ?>
@@ -159,6 +167,13 @@ final class VersionAdminController
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em> <em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td>
<?php if ($version->getFileHash()): ?>
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
<?php else: ?>
<em>—</em>
<?php endif; ?>
</td>
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td> <td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
<td> <td>
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>"> <span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
@@ -218,6 +233,8 @@ final class VersionAdminController
'error' => __('An error occurred. Please try again.', 'wc-licensed-product'), 'error' => __('An error occurred. Please try again.', 'wc-licensed-product'),
'selectFile' => __('Select Download File', 'wc-licensed-product'), 'selectFile' => __('Select Download File', 'wc-licensed-product'),
'useThisFile' => __('Use this file', 'wc-licensed-product'), 'useThisFile' => __('Use this file', 'wc-licensed-product'),
'invalidChecksumFile' => __('Invalid checksum file format. File must contain a 64-character SHA256 hash.', 'wc-licensed-product'),
'checksumReadError' => __('Failed to read checksum file.', 'wc-licensed-product'),
], ],
]); ]);
@@ -242,9 +259,9 @@ final class VersionAdminController
$productId = absint($_POST['product_id'] ?? 0); $productId = absint($_POST['product_id'] ?? 0);
$version = sanitize_text_field($_POST['version'] ?? ''); $version = sanitize_text_field($_POST['version'] ?? '');
$downloadUrl = esc_url_raw($_POST['download_url'] ?? '');
$releaseNotes = sanitize_textarea_field($_POST['release_notes'] ?? ''); $releaseNotes = sanitize_textarea_field($_POST['release_notes'] ?? '');
$attachmentId = absint($_POST['attachment_id'] ?? 0); $attachmentId = absint($_POST['attachment_id'] ?? 0);
$fileHash = sanitize_text_field($_POST['file_hash'] ?? '');
if (!$productId || !$version) { if (!$productId || !$version) {
wp_send_json_error(['message' => __('Product ID and version are required.', 'wc-licensed-product')]); wp_send_json_error(['message' => __('Product ID and version are required.', 'wc-licensed-product')]);
@@ -270,13 +287,17 @@ final class VersionAdminController
wp_send_json_error(['message' => __('This product is not a licensed product.', 'wc-licensed-product')]); wp_send_json_error(['message' => __('This product is not a licensed product.', 'wc-licensed-product')]);
} }
try {
$newVersion = $this->versionManager->createVersion( $newVersion = $this->versionManager->createVersion(
$productId, $productId,
$version, $version,
$releaseNotes ?: null, $releaseNotes ?: null,
$downloadUrl ?: null, $attachmentId ?: null,
$attachmentId ?: null $fileHash ?: null
); );
} catch (\InvalidArgumentException $e) {
wp_send_json_error(['message' => $e->getMessage()]);
}
if (!$newVersion) { if (!$newVersion) {
global $wpdb; global $wpdb;
@@ -375,6 +396,13 @@ final class VersionAdminController
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em> <em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td>
<?php if ($version->getFileHash()): ?>
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
<?php else: ?>
<em>—</em>
<?php endif; ?>
</td>
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td> <td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
<td> <td>
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>"> <span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">

128
src/Api/ResponseSigner.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
/**
* Response Signer
*
* Signs REST API responses to prevent tampering and replay attacks.
*
* @package Jeremias\WcLicensedProduct\Api
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Api;
/**
* Signs license API responses using HMAC-SHA256
*
* The security model:
* 1. Server generates a unique signature for each response using HMAC-SHA256
* 2. Signature includes a timestamp to prevent replay attacks
* 3. Client verifies the signature using a shared secret
* 4. Invalid signatures cause the client to reject the response
*/
final class ResponseSigner
{
private string $serverSecret;
public function __construct()
{
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET')
? WC_LICENSE_SERVER_SECRET
: '';
}
/**
* Register WordPress hooks
*/
public function register(): void
{
add_filter('rest_post_dispatch', [$this, 'signResponse'], 10, 3);
}
/**
* Sign REST API response
*
* @param \WP_REST_Response $response The response object
* @param \WP_REST_Server $server The REST server
* @param \WP_REST_Request $request The request object
* @return \WP_REST_Response
*/
public function signResponse($response, $server, $request)
{
// Only sign license API responses
if (!$this->shouldSign($request)) {
return $response;
}
$data = $response->get_data();
$licenseKey = $request->get_param('license_key');
if (empty($licenseKey) || !is_array($data) || empty($this->serverSecret)) {
return $response;
}
$headers = $this->createSignatureHeaders($data, $licenseKey);
foreach ($headers as $name => $value) {
$response->header($name, $value);
}
return $response;
}
/**
* Check if request should be signed
*/
private function shouldSign(\WP_REST_Request $request): bool
{
$route = $request->get_route();
return str_starts_with($route, '/wc-licensed-product/v1/validate')
|| str_starts_with($route, '/wc-licensed-product/v1/status')
|| str_starts_with($route, '/wc-licensed-product/v1/activate');
}
/**
* Create signature headers for response
*
* @param array $data The response data
* @param string $licenseKey The license key from the request
* @return array Associative array of headers
*/
private function createSignatureHeaders(array $data, string $licenseKey): array
{
$timestamp = time();
$signingKey = $this->deriveKey($licenseKey);
// Sort keys for consistent ordering
ksort($data);
// Build signature payload
$payload = $timestamp . ':' . json_encode(
$data,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
return [
'X-License-Signature' => hash_hmac('sha256', $payload, $signingKey),
'X-License-Timestamp' => (string) $timestamp,
];
}
/**
* Derive a unique signing key for a license
*
* Uses HKDF-like key derivation to create a unique signing key
* for each license key, preventing cross-license signature attacks.
*
* @param string $licenseKey The license key
* @return string The derived signing key (hex encoded)
*/
private function deriveKey(string $licenseKey): string
{
// HKDF-like key derivation
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
}
}

View File

@@ -129,6 +129,7 @@ final class AccountController
), ),
'release_notes' => $version->getReleaseNotes(), 'release_notes' => $version->getReleaseNotes(),
'released_at' => $version->getReleasedAt()->format(get_option('date_format')), 'released_at' => $version->getReleasedAt()->format(get_option('date_format')),
'file_hash' => $version->getFileHash(),
]; ];
} }
} }

View File

@@ -102,6 +102,7 @@ final class Installer
release_notes TEXT DEFAULT NULL, release_notes TEXT DEFAULT NULL,
download_url VARCHAR(512) DEFAULT NULL, download_url VARCHAR(512) DEFAULT NULL,
attachment_id BIGINT UNSIGNED DEFAULT NULL, attachment_id BIGINT UNSIGNED DEFAULT NULL,
file_hash VARCHAR(64) DEFAULT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1, is_active TINYINT(1) NOT NULL DEFAULT 1,
released_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, released_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@@ -13,6 +13,7 @@ use Jeremias\WcLicensedProduct\Admin\AdminController;
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController; use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
use Jeremias\WcLicensedProduct\Admin\SettingsController; use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\Admin\VersionAdminController; use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
use Jeremias\WcLicensedProduct\Api\RestApiController; use Jeremias\WcLicensedProduct\Api\RestApiController;
use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration; use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration;
use Jeremias\WcLicensedProduct\Checkout\CheckoutController; use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
@@ -128,6 +129,11 @@ final class Plugin
new RestApiController($this->licenseManager); new RestApiController($this->licenseManager);
new LicenseEmailController($this->licenseManager); new LicenseEmailController($this->licenseManager);
// Initialize response signing if server secret is configured
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') {
(new ResponseSigner())->register();
}
if (is_admin()) { if (is_admin()) {
new AdminController($this->twig, $this->licenseManager); new AdminController($this->twig, $this->licenseManager);
new VersionAdminController($this->versionManager); new VersionAdminController($this->versionManager);

View File

@@ -23,6 +23,7 @@ class ProductVersion
private ?string $releaseNotes; private ?string $releaseNotes;
private ?string $downloadUrl; private ?string $downloadUrl;
private ?int $attachmentId; private ?int $attachmentId;
private ?string $fileHash;
private bool $isActive; private bool $isActive;
private \DateTimeInterface $releasedAt; private \DateTimeInterface $releasedAt;
private \DateTimeInterface $createdAt; private \DateTimeInterface $createdAt;
@@ -42,6 +43,7 @@ class ProductVersion
$version->releaseNotes = $data['release_notes'] ?: null; $version->releaseNotes = $data['release_notes'] ?: null;
$version->downloadUrl = $data['download_url'] ?: null; $version->downloadUrl = $data['download_url'] ?: null;
$version->attachmentId = !empty($data['attachment_id']) ? (int) $data['attachment_id'] : null; $version->attachmentId = !empty($data['attachment_id']) ? (int) $data['attachment_id'] : null;
$version->fileHash = $data['file_hash'] ?? null;
$version->isActive = (bool) $data['is_active']; $version->isActive = (bool) $data['is_active'];
$version->releasedAt = new \DateTimeImmutable($data['released_at']); $version->releasedAt = new \DateTimeImmutable($data['released_at']);
$version->createdAt = new \DateTimeImmutable($data['created_at']); $version->createdAt = new \DateTimeImmutable($data['created_at']);
@@ -137,15 +139,20 @@ class ProductVersion
return $this->attachmentId; return $this->attachmentId;
} }
public function getFileHash(): ?string
{
return $this->fileHash;
}
/** /**
* Get the effective download URL (from attachment or direct URL) * Get the download URL from attachment
*/ */
public function getEffectiveDownloadUrl(): ?string public function getEffectiveDownloadUrl(): ?string
{ {
if ($this->attachmentId) { if ($this->attachmentId) {
return wp_get_attachment_url($this->attachmentId) ?: null; return wp_get_attachment_url($this->attachmentId) ?: null;
} }
return $this->downloadUrl; return null;
} }
/** /**
@@ -156,9 +163,6 @@ class ProductVersion
if ($this->attachmentId) { if ($this->attachmentId) {
return wp_basename(get_attached_file($this->attachmentId) ?: ''); return wp_basename(get_attached_file($this->attachmentId) ?: '');
} }
if ($this->downloadUrl) {
return wp_basename($this->downloadUrl);
}
return null; return null;
} }
@@ -192,6 +196,7 @@ class ProductVersion
'release_notes' => $this->releaseNotes, 'release_notes' => $this->releaseNotes,
'download_url' => $this->downloadUrl, 'download_url' => $this->downloadUrl,
'attachment_id' => $this->attachmentId, 'attachment_id' => $this->attachmentId,
'file_hash' => $this->fileHash,
'is_active' => $this->isActive, 'is_active' => $this->isActive,
'released_at' => $this->releasedAt->format('Y-m-d H:i:s'), 'released_at' => $this->releasedAt->format('Y-m-d H:i:s'),
'created_at' => $this->createdAt->format('Y-m-d H:i:s'), 'created_at' => $this->createdAt->format('Y-m-d H:i:s'),

View File

@@ -92,16 +92,27 @@ class VersionManager
/** /**
* Create a new version * Create a new version
*
* @throws \InvalidArgumentException If file hash validation fails
*/ */
public function createVersion( public function createVersion(
int $productId, int $productId,
string $version, string $version,
?string $releaseNotes = null, ?string $releaseNotes = null,
?string $downloadUrl = null, ?int $attachmentId = null,
?int $attachmentId = null ?string $fileHash = null
): ?ProductVersion { ): ?ProductVersion {
global $wpdb; global $wpdb;
// Validate file hash if both attachment and hash are provided
if ($attachmentId !== null && $attachmentId > 0 && $fileHash !== null && $fileHash !== '') {
$validatedHash = $this->validateFileHash($attachmentId, $fileHash);
if ($validatedHash === false) {
return null;
}
$fileHash = $validatedHash;
}
$parsed = ProductVersion::parseVersion($version); $parsed = ProductVersion::parseVersion($version);
$tableName = Installer::getVersionsTable(); $tableName = Installer::getVersionsTable();
@@ -114,10 +125,9 @@ class VersionManager
'minor_version' => $parsed['minor'], 'minor_version' => $parsed['minor'],
'patch_version' => $parsed['patch'], 'patch_version' => $parsed['patch'],
'release_notes' => $releaseNotes, 'release_notes' => $releaseNotes,
'download_url' => $downloadUrl,
'is_active' => 1, 'is_active' => 1,
]; ];
$formats = ['%d', '%s', '%d', '%d', '%d', '%s', '%s', '%d']; $formats = ['%d', '%s', '%d', '%d', '%d', '%s', '%d'];
// Only include attachment_id if it's set // Only include attachment_id if it's set
if ($attachmentId !== null && $attachmentId > 0) { if ($attachmentId !== null && $attachmentId > 0) {
@@ -125,6 +135,12 @@ class VersionManager
$formats[] = '%d'; $formats[] = '%d';
} }
// Only include file_hash if it's set
if ($fileHash !== null && $fileHash !== '') {
$data['file_hash'] = $fileHash;
$formats[] = '%s';
}
$result = $wpdb->insert($tableName, $data, $formats); $result = $wpdb->insert($tableName, $data, $formats);
if ($result === false) { if ($result === false) {
@@ -136,13 +152,44 @@ class VersionManager
return $this->getVersionById((int) $wpdb->insert_id); return $this->getVersionById((int) $wpdb->insert_id);
} }
/**
* Validate file hash against attachment
*
* @return string|false The validated hash (lowercase) or false on mismatch
* @throws \InvalidArgumentException If hash doesn't match
*/
private function validateFileHash(int $attachmentId, string $providedHash): string|false
{
$filePath = get_attached_file($attachmentId);
if (!$filePath || !file_exists($filePath)) {
throw new \InvalidArgumentException(
__('Attachment file not found.', 'wc-licensed-product')
);
}
$calculatedHash = hash_file('sha256', $filePath);
$providedHash = strtolower(trim($providedHash));
if (!hash_equals($calculatedHash, $providedHash)) {
throw new \InvalidArgumentException(
sprintf(
/* translators: 1: provided hash, 2: calculated hash */
__('File checksum does not match. Expected: %1$s, Got: %2$s', 'wc-licensed-product'),
$providedHash,
$calculatedHash
)
);
}
return $calculatedHash;
}
/** /**
* Update a version * Update a version
*/ */
public function updateVersion( public function updateVersion(
int $versionId, int $versionId,
?string $releaseNotes = null, ?string $releaseNotes = null,
?string $downloadUrl = null,
?bool $isActive = null, ?bool $isActive = null,
?int $attachmentId = null ?int $attachmentId = null
): bool { ): bool {
@@ -156,11 +203,6 @@ class VersionManager
$formats[] = '%s'; $formats[] = '%s';
} }
if ($downloadUrl !== null) {
$data['download_url'] = $downloadUrl;
$formats[] = '%s';
}
if ($isActive !== null) { if ($isActive !== null) {
$data['is_active'] = $isActive ? 1 : 0; $data['is_active'] = $isActive ? 1 : 0;
$formats[] = '%d'; $formats[] = '%d';

View File

@@ -64,6 +64,12 @@
</a> </a>
<span class="download-version">v{{ esc_html(download.version) }}</span> <span class="download-version">v{{ esc_html(download.version) }}</span>
<span class="download-date">{{ esc_html(download.released_at) }}</span> <span class="download-date">{{ esc_html(download.released_at) }}</span>
{% if download.file_hash %}
<span class="download-hash" title="{{ esc_attr(download.file_hash) }}">
<span class="dashicons dashicons-shield"></span>
<code>{{ download.file_hash[:12] }}...</code>
</span>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -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.1.0 * Version: 0.2.2
* 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.1.0'); define('WC_LICENSED_PRODUCT_VERSION', '0.2.2');
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__));