You've already forked wc-licensed-product
364 lines
9.7 KiB
PHP
364 lines
9.7 KiB
PHP
|
|
<?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;
|
||
|
|
}
|
||
|
|
}
|