You've already forked wc-licensed-product
Implement version 0.0.1 - Licensed Product type for WooCommerce
Add complete plugin infrastructure for selling software with license keys: - New "Licensed Product" WooCommerce product type - License key generation (XXXX-XXXX-XXXX-XXXX format) on order completion - Domain-based license validation system - REST API endpoints (validate, status, activate, deactivate) - Customer My Account "Licenses" page - Admin license management under WooCommerce > Licenses - Checkout domain field for licensed products - Custom database tables for licenses and product versions - Twig template engine integration - Full i18n support with German (de_CH) translation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
363
src/License/LicenseManager.php
Normal file
363
src/License/LicenseManager.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
/**
|
||||
* License Manager
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\License;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Installer;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
|
||||
|
||||
/**
|
||||
* Manages license operations (CRUD, validation, generation)
|
||||
*/
|
||||
class LicenseManager
|
||||
{
|
||||
/**
|
||||
* Generate a unique license key
|
||||
*/
|
||||
public function generateLicenseKey(): string
|
||||
{
|
||||
// Format: XXXX-XXXX-XXXX-XXXX (32 chars hex, 4 groups)
|
||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$key = '';
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
if ($i > 0) {
|
||||
$key .= '-';
|
||||
}
|
||||
for ($j = 0; $j < 4; $j++) {
|
||||
$key .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a license for a completed order
|
||||
*/
|
||||
public function generateLicense(
|
||||
int $orderId,
|
||||
int $productId,
|
||||
int $customerId,
|
||||
string $domain
|
||||
): ?License {
|
||||
global $wpdb;
|
||||
|
||||
// Check if license already exists for this order and product
|
||||
$existing = $this->getLicenseByOrderAndProduct($orderId, $productId);
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$product = wc_get_product($productId);
|
||||
if (!$product instanceof LicensedProduct) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate unique license key
|
||||
$licenseKey = $this->generateLicenseKey();
|
||||
while ($this->getLicenseByKey($licenseKey)) {
|
||||
$licenseKey = $this->generateLicenseKey();
|
||||
}
|
||||
|
||||
// Calculate expiration date
|
||||
$expiresAt = null;
|
||||
$validityDays = $product->get_validity_days();
|
||||
if ($validityDays !== null && $validityDays > 0) {
|
||||
$expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// Determine version ID if bound to version
|
||||
$versionId = null;
|
||||
if ($product->is_bound_to_version()) {
|
||||
$versionId = $this->getCurrentVersionId($productId);
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->insert(
|
||||
$tableName,
|
||||
[
|
||||
'license_key' => $licenseKey,
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $productId,
|
||||
'customer_id' => $customerId,
|
||||
'domain' => $this->normalizeDomain($domain),
|
||||
'version_id' => $versionId,
|
||||
'status' => License::STATUS_ACTIVE,
|
||||
'activations_count' => 1,
|
||||
'max_activations' => $product->get_max_activations(),
|
||||
'expires_at' => $expiresAt,
|
||||
],
|
||||
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getLicenseById((int) $wpdb->insert_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by ID
|
||||
*/
|
||||
public function getLicenseById(int $id): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare("SELECT * FROM {$tableName} WHERE id = %d", $id),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by license key
|
||||
*/
|
||||
public function getLicenseByKey(string $licenseKey): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare("SELECT * FROM {$tableName} WHERE license_key = %s", $licenseKey),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by order and product
|
||||
*/
|
||||
public function getLicenseByOrderAndProduct(int $orderId, int $productId): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d",
|
||||
$orderId,
|
||||
$productId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses for a customer
|
||||
*/
|
||||
public function getLicensesByCustomer(int $customerId): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} WHERE customer_id = %d ORDER BY created_at DESC",
|
||||
$customerId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses (for admin)
|
||||
*/
|
||||
public function getAllLicenses(int $page = 1, int $perPage = 20): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$perPage,
|
||||
$offset
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total license count
|
||||
*/
|
||||
public function getLicenseCount(): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a license key for a domain
|
||||
*/
|
||||
public function validateLicense(string $licenseKey, string $domain): array
|
||||
{
|
||||
$license = $this->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_not_found',
|
||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check license status
|
||||
if ($license->getStatus() === License::STATUS_REVOKED) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_revoked',
|
||||
'message' => __('This license has been revoked.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if ($license->isExpired()) {
|
||||
$this->updateLicenseStatus($license->getId(), License::STATUS_EXPIRED);
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_expired',
|
||||
'message' => __('This license has expired.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($license->getStatus() === License::STATUS_INACTIVE) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_inactive',
|
||||
'message' => __('This license is inactive.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check domain
|
||||
$normalizedDomain = $this->normalizeDomain($domain);
|
||||
if ($license->getDomain() !== $normalizedDomain) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'domain_mismatch',
|
||||
'message' => __('This license is not valid for this domain.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'license' => [
|
||||
'product_id' => $license->getProductId(),
|
||||
'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
|
||||
'version_id' => $license->getVersionId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license status
|
||||
*/
|
||||
public function updateLicenseStatus(int $licenseId, string $status): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['status' => $status],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license domain
|
||||
*/
|
||||
public function updateLicenseDomain(int $licenseId, string $domain): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['domain' => $this->normalizeDomain($domain)],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a license
|
||||
*/
|
||||
public function deleteLicense(int $licenseId): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->delete(
|
||||
$tableName,
|
||||
['id' => $licenseId],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize domain name
|
||||
*/
|
||||
public function normalizeDomain(string $domain): string
|
||||
{
|
||||
// Remove protocol
|
||||
$domain = preg_replace('#^https?://#', '', $domain);
|
||||
|
||||
// Remove trailing slash and path
|
||||
$domain = preg_replace('#/.*$#', '', $domain);
|
||||
|
||||
// Remove www prefix
|
||||
$domain = preg_replace('#^www\.#', '', $domain);
|
||||
|
||||
// Lowercase
|
||||
return strtolower(trim($domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version ID for a product
|
||||
*/
|
||||
private function getCurrentVersionId(int $productId): ?int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getVersionsTable();
|
||||
$versionId = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$tableName} WHERE product_id = %d AND is_active = 1 ORDER BY released_at DESC LIMIT 1",
|
||||
$productId
|
||||
)
|
||||
);
|
||||
|
||||
return $versionId ? (int) $versionId : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user