2026-01-21 18:55:18 +01:00
|
|
|
<?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);
|
2026-01-21 21:58:54 +01:00
|
|
|
if (!$product || !$product->is_type('licensed')) {
|
2026-01-21 18:55:18 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 21:58:54 +01:00
|
|
|
// Ensure we have the LicensedProduct instance for type hints
|
|
|
|
|
if (!$product instanceof LicensedProduct) {
|
|
|
|
|
$product = new LicensedProduct($productId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 22:18:27 +01:00
|
|
|
/**
|
|
|
|
|
* Get all licenses for an order
|
|
|
|
|
*/
|
|
|
|
|
public function getLicensesByOrder(int $orderId): array
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$rows = $wpdb->get_results(
|
|
|
|
|
$wpdb->prepare(
|
|
|
|
|
"SELECT * FROM {$tableName} WHERE order_id = %d ORDER BY created_at DESC",
|
|
|
|
|
$orderId
|
|
|
|
|
),
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* 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 ?: []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-21 20:32:35 +01:00
|
|
|
* Get all licenses (for admin) with optional filtering
|
|
|
|
|
*
|
|
|
|
|
* @param int $page Page number
|
|
|
|
|
* @param int $perPage Items per page
|
|
|
|
|
* @param array $filters Optional filters: search, status, product_id, customer_id
|
|
|
|
|
* @return array Array of License objects
|
2026-01-21 18:55:18 +01:00
|
|
|
*/
|
2026-01-21 20:32:35 +01:00
|
|
|
public function getAllLicenses(int $page = 1, int $perPage = 20, array $filters = []): array
|
2026-01-21 18:55:18 +01:00
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$offset = ($page - 1) * $perPage;
|
|
|
|
|
|
2026-01-21 20:32:35 +01:00
|
|
|
$where = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
// Search filter (searches license key, domain, customer email)
|
|
|
|
|
if (!empty($filters['search'])) {
|
|
|
|
|
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
|
|
|
|
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
|
|
|
|
$params[] = $search;
|
|
|
|
|
$params[] = $search;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status filter
|
|
|
|
|
if (!empty($filters['status']) && in_array($filters['status'], [
|
|
|
|
|
License::STATUS_ACTIVE,
|
|
|
|
|
License::STATUS_INACTIVE,
|
|
|
|
|
License::STATUS_EXPIRED,
|
|
|
|
|
License::STATUS_REVOKED,
|
|
|
|
|
], true)) {
|
|
|
|
|
$where[] = "status = %s";
|
|
|
|
|
$params[] = $filters['status'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Product filter
|
|
|
|
|
if (!empty($filters['product_id'])) {
|
|
|
|
|
$where[] = "product_id = %d";
|
|
|
|
|
$params[] = absint($filters['product_id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Customer filter
|
|
|
|
|
if (!empty($filters['customer_id'])) {
|
|
|
|
|
$where[] = "customer_id = %d";
|
|
|
|
|
$params[] = absint($filters['customer_id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
|
|
|
|
|
|
$params[] = $perPage;
|
|
|
|
|
$params[] = $offset;
|
|
|
|
|
|
|
|
|
|
$sql = "SELECT * FROM {$tableName} {$whereClause} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
$rows = $wpdb->get_results(
|
2026-01-21 20:32:35 +01:00
|
|
|
$wpdb->prepare($sql, $params),
|
2026-01-21 18:55:18 +01:00
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-21 20:32:35 +01:00
|
|
|
* Get total license count with optional filtering
|
|
|
|
|
*
|
|
|
|
|
* @param array $filters Optional filters: search, status, product_id, customer_id
|
|
|
|
|
* @return int Total count
|
|
|
|
|
*/
|
|
|
|
|
public function getLicenseCount(array $filters = []): int
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
|
|
|
|
|
$where = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
// Search filter
|
|
|
|
|
if (!empty($filters['search'])) {
|
|
|
|
|
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
|
|
|
|
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
|
|
|
|
$params[] = $search;
|
|
|
|
|
$params[] = $search;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Status filter
|
|
|
|
|
if (!empty($filters['status']) && in_array($filters['status'], [
|
|
|
|
|
License::STATUS_ACTIVE,
|
|
|
|
|
License::STATUS_INACTIVE,
|
|
|
|
|
License::STATUS_EXPIRED,
|
|
|
|
|
License::STATUS_REVOKED,
|
|
|
|
|
], true)) {
|
|
|
|
|
$where[] = "status = %s";
|
|
|
|
|
$params[] = $filters['status'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Product filter
|
|
|
|
|
if (!empty($filters['product_id'])) {
|
|
|
|
|
$where[] = "product_id = %d";
|
|
|
|
|
$params[] = absint($filters['product_id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Customer filter
|
|
|
|
|
if (!empty($filters['customer_id'])) {
|
|
|
|
|
$where[] = "customer_id = %d";
|
|
|
|
|
$params[] = absint($filters['customer_id']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
|
|
|
|
|
|
if (empty($params)) {
|
|
|
|
|
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (int) $wpdb->get_var(
|
|
|
|
|
$wpdb->prepare("SELECT COUNT(*) FROM {$tableName} {$whereClause}", $params)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all licensed products for filter dropdown
|
|
|
|
|
*
|
|
|
|
|
* @return array Array of [id => name] pairs
|
2026-01-21 18:55:18 +01:00
|
|
|
*/
|
2026-01-21 20:32:35 +01:00
|
|
|
public function getLicensedProducts(): array
|
2026-01-21 18:55:18 +01:00
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
2026-01-21 20:32:35 +01:00
|
|
|
$productIds = $wpdb->get_col("SELECT DISTINCT product_id FROM {$tableName}");
|
|
|
|
|
|
|
|
|
|
$products = [];
|
|
|
|
|
foreach ($productIds as $productId) {
|
|
|
|
|
$product = wc_get_product((int) $productId);
|
|
|
|
|
if ($product) {
|
|
|
|
|
$products[(int) $productId] = $product->get_name();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $products;
|
2026-01-21 18:55:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
2026-01-21 20:32:35 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extend license expiration
|
|
|
|
|
*
|
|
|
|
|
* @param int $licenseId License ID
|
|
|
|
|
* @param int $days Number of days to extend
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public function extendLicense(int $licenseId, int $days): bool
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$license = $this->getLicenseById($licenseId);
|
|
|
|
|
if (!$license) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate new expiration date
|
|
|
|
|
$currentExpiry = $license->getExpiresAt();
|
|
|
|
|
if ($currentExpiry === null) {
|
|
|
|
|
// License is lifetime, set expiration from now
|
|
|
|
|
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
|
|
|
|
} elseif ($currentExpiry < new \DateTimeImmutable()) {
|
|
|
|
|
// License is expired, extend from now
|
|
|
|
|
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
|
|
|
|
} else {
|
|
|
|
|
// License still valid, extend from current expiry
|
|
|
|
|
$newExpiry = \DateTimeImmutable::createFromInterface($currentExpiry)->modify("+{$days} days");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$result = $wpdb->update(
|
|
|
|
|
$tableName,
|
|
|
|
|
['expires_at' => $newExpiry->format('Y-m-d H:i:s')],
|
|
|
|
|
['id' => $licenseId],
|
|
|
|
|
['%s'],
|
|
|
|
|
['%d']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If license was expired, reactivate it
|
|
|
|
|
if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED) {
|
|
|
|
|
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set license to lifetime (no expiration)
|
|
|
|
|
*
|
|
|
|
|
* @param int $licenseId License ID
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public function setLicenseLifetime(int $licenseId): bool
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$license = $this->getLicenseById($licenseId);
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
// Use raw query to set NULL
|
|
|
|
|
$result = $wpdb->query(
|
|
|
|
|
$wpdb->prepare(
|
|
|
|
|
"UPDATE {$tableName} SET expires_at = NULL WHERE id = %d",
|
|
|
|
|
$licenseId
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If license was expired, reactivate it
|
|
|
|
|
if ($result !== false && $license && $license->getStatus() === License::STATUS_EXPIRED) {
|
|
|
|
|
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk update license status
|
|
|
|
|
*
|
|
|
|
|
* @param array $licenseIds Array of license IDs
|
|
|
|
|
* @param string $status New status
|
|
|
|
|
* @return int Number of licenses updated
|
|
|
|
|
*/
|
|
|
|
|
public function bulkUpdateStatus(array $licenseIds, string $status): int
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
if (empty($licenseIds)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$ids = array_map('absint', $licenseIds);
|
|
|
|
|
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
|
|
|
|
|
|
|
|
|
$result = $wpdb->query(
|
|
|
|
|
$wpdb->prepare(
|
|
|
|
|
"UPDATE {$tableName} SET status = %s WHERE id IN ({$placeholders})",
|
|
|
|
|
array_merge([$status], $ids)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $result !== false ? (int) $result : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk delete licenses
|
|
|
|
|
*
|
|
|
|
|
* @param array $licenseIds Array of license IDs
|
|
|
|
|
* @return int Number of licenses deleted
|
|
|
|
|
*/
|
|
|
|
|
public function bulkDelete(array $licenseIds): int
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
if (empty($licenseIds)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$ids = array_map('absint', $licenseIds);
|
|
|
|
|
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
|
|
|
|
|
|
|
|
|
$result = $wpdb->query(
|
|
|
|
|
$wpdb->prepare(
|
|
|
|
|
"DELETE FROM {$tableName} WHERE id IN ({$placeholders})",
|
|
|
|
|
$ids
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $result !== false ? (int) $result : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk extend licenses
|
|
|
|
|
*
|
|
|
|
|
* @param array $licenseIds Array of license IDs
|
|
|
|
|
* @param int $days Number of days to extend
|
|
|
|
|
* @return int Number of licenses extended
|
|
|
|
|
*/
|
|
|
|
|
public function bulkExtend(array $licenseIds, int $days): int
|
|
|
|
|
{
|
|
|
|
|
$count = 0;
|
|
|
|
|
foreach ($licenseIds as $licenseId) {
|
|
|
|
|
if ($this->extendLicense((int) $licenseId, $days)) {
|
|
|
|
|
$count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Transfer license to a new domain
|
|
|
|
|
*
|
|
|
|
|
* @param int $licenseId License ID
|
|
|
|
|
* @param string $newDomain New domain to transfer to
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public function transferLicense(int $licenseId, string $newDomain): bool
|
|
|
|
|
{
|
|
|
|
|
$license = $this->getLicenseById($licenseId);
|
|
|
|
|
if (!$license) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cannot transfer revoked licenses
|
|
|
|
|
if ($license->getStatus() === License::STATUS_REVOKED) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->updateLicenseDomain($licenseId, $newDomain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get license statistics
|
|
|
|
|
*
|
|
|
|
|
* @return array Statistics data
|
|
|
|
|
*/
|
|
|
|
|
public function getStatistics(): array
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
|
|
|
|
|
// Get counts by status
|
|
|
|
|
$statusCounts = $wpdb->get_results(
|
|
|
|
|
"SELECT status, COUNT(*) as count FROM {$tableName} GROUP BY status",
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$byStatus = [
|
|
|
|
|
License::STATUS_ACTIVE => 0,
|
|
|
|
|
License::STATUS_INACTIVE => 0,
|
|
|
|
|
License::STATUS_EXPIRED => 0,
|
|
|
|
|
License::STATUS_REVOKED => 0,
|
|
|
|
|
];
|
|
|
|
|
foreach ($statusCounts ?: [] as $row) {
|
|
|
|
|
$byStatus[$row['status']] = (int) $row['count'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get total count
|
|
|
|
|
$total = array_sum($byStatus);
|
|
|
|
|
|
|
|
|
|
// Get lifetime vs expiring licenses
|
|
|
|
|
$lifetimeCount = (int) $wpdb->get_var(
|
|
|
|
|
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NULL"
|
|
|
|
|
);
|
|
|
|
|
$expiringCount = $total - $lifetimeCount;
|
|
|
|
|
|
|
|
|
|
// Get licenses expiring soon (next 30 days)
|
|
|
|
|
$expiringSoon = (int) $wpdb->get_var(
|
|
|
|
|
$wpdb->prepare(
|
|
|
|
|
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NOT NULL AND expires_at <= %s AND expires_at > NOW() AND status = %s",
|
|
|
|
|
(new \DateTimeImmutable())->modify('+30 days')->format('Y-m-d H:i:s'),
|
|
|
|
|
License::STATUS_ACTIVE
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get licenses by product
|
|
|
|
|
$byProduct = $wpdb->get_results(
|
|
|
|
|
"SELECT product_id, COUNT(*) as count FROM {$tableName} GROUP BY product_id ORDER BY count DESC LIMIT 10",
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$productStats = [];
|
|
|
|
|
foreach ($byProduct ?: [] as $row) {
|
|
|
|
|
$product = wc_get_product((int) $row['product_id']);
|
|
|
|
|
$productStats[] = [
|
|
|
|
|
'product_id' => (int) $row['product_id'],
|
|
|
|
|
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
|
|
|
|
'count' => (int) $row['count'],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get licenses created per month (last 12 months)
|
|
|
|
|
$monthlyData = $wpdb->get_results(
|
|
|
|
|
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count
|
|
|
|
|
FROM {$tableName}
|
|
|
|
|
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
|
|
|
|
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
|
|
|
|
ORDER BY month ASC",
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$monthlyStats = [];
|
|
|
|
|
foreach ($monthlyData ?: [] as $row) {
|
|
|
|
|
$monthlyStats[$row['month']] = (int) $row['count'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get top domains
|
|
|
|
|
$topDomains = $wpdb->get_results(
|
|
|
|
|
"SELECT domain, COUNT(*) as count FROM {$tableName} GROUP BY domain ORDER BY count DESC LIMIT 10",
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'total' => $total,
|
|
|
|
|
'by_status' => $byStatus,
|
|
|
|
|
'lifetime' => $lifetimeCount,
|
|
|
|
|
'expiring' => $expiringCount,
|
|
|
|
|
'expiring_soon' => $expiringSoon,
|
|
|
|
|
'by_product' => $productStats,
|
|
|
|
|
'monthly' => $monthlyStats,
|
|
|
|
|
'top_domains' => $topDomains ?: [],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get licenses expiring within specified days
|
|
|
|
|
*
|
|
|
|
|
* @param int $days Number of days to look ahead
|
|
|
|
|
* @param bool $excludeNotified Whether to exclude already notified licenses
|
|
|
|
|
* @return array Array of License objects with customer data
|
|
|
|
|
*/
|
|
|
|
|
public function getLicensesExpiringSoon(int $days = 7, bool $excludeNotified = true): array
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$now = new \DateTimeImmutable();
|
|
|
|
|
$future = $now->modify("+{$days} days");
|
|
|
|
|
|
|
|
|
|
$sql = "SELECT * FROM {$tableName}
|
|
|
|
|
WHERE expires_at IS NOT NULL
|
|
|
|
|
AND expires_at > %s
|
|
|
|
|
AND expires_at <= %s
|
|
|
|
|
AND status = %s";
|
|
|
|
|
|
|
|
|
|
$params = [
|
|
|
|
|
$now->format('Y-m-d H:i:s'),
|
|
|
|
|
$future->format('Y-m-d H:i:s'),
|
|
|
|
|
License::STATUS_ACTIVE,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$rows = $wpdb->get_results(
|
|
|
|
|
$wpdb->prepare($sql, $params),
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark license as notified for expiration warning
|
|
|
|
|
*
|
|
|
|
|
* @param int $licenseId License ID
|
|
|
|
|
* @param string $notificationType Type of notification (e.g., 'expiring_7_days', 'expiring_1_day')
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public function markExpirationNotified(int $licenseId, string $notificationType): bool
|
|
|
|
|
{
|
|
|
|
|
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType);
|
|
|
|
|
update_user_meta($this->getLicenseById($licenseId)?->getCustomerId() ?? 0, $metaKey . '_' . $licenseId, current_time('mysql'));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if license was already notified for expiration
|
|
|
|
|
*
|
|
|
|
|
* @param int $licenseId License ID
|
|
|
|
|
* @param string $notificationType Type of notification
|
|
|
|
|
* @return bool Whether already notified
|
|
|
|
|
*/
|
|
|
|
|
public function wasExpirationNotified(int $licenseId, string $notificationType): bool
|
|
|
|
|
{
|
|
|
|
|
$license = $this->getLicenseById($licenseId);
|
|
|
|
|
if (!$license) {
|
|
|
|
|
return true; // Consider notified if license doesn't exist
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType) . '_' . $licenseId;
|
|
|
|
|
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Import a license from CSV data
|
|
|
|
|
*
|
|
|
|
|
* @param string $licenseKey License key
|
|
|
|
|
* @param int $productId Product ID
|
|
|
|
|
* @param int $customerId Customer ID
|
|
|
|
|
* @param string $domain Domain name
|
|
|
|
|
* @param int $orderId Order ID (optional)
|
|
|
|
|
* @param string $status License status
|
|
|
|
|
* @param int $maxActivations Maximum activations
|
|
|
|
|
* @param int $activationsCount Current activation count
|
|
|
|
|
* @param \DateTimeImmutable|null $expiresAt Expiration date or null for lifetime
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
public function importLicense(
|
|
|
|
|
string $licenseKey,
|
|
|
|
|
int $productId,
|
|
|
|
|
int $customerId,
|
|
|
|
|
string $domain,
|
|
|
|
|
int $orderId = 0,
|
|
|
|
|
string $status = License::STATUS_ACTIVE,
|
|
|
|
|
int $maxActivations = 1,
|
|
|
|
|
int $activationsCount = 1,
|
|
|
|
|
?\DateTimeImmutable $expiresAt = null
|
|
|
|
|
): bool {
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$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' => null,
|
|
|
|
|
'status' => $status,
|
|
|
|
|
'activations_count' => $activationsCount,
|
|
|
|
|
'max_activations' => $maxActivations,
|
|
|
|
|
'expires_at' => $expiresAt ? $expiresAt->format('Y-m-d H:i:s') : null,
|
|
|
|
|
],
|
|
|
|
|
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Export all licenses to array format suitable for CSV
|
|
|
|
|
*
|
|
|
|
|
* @return array Array of license data for CSV export
|
|
|
|
|
*/
|
|
|
|
|
public function exportLicensesForCsv(): array
|
|
|
|
|
{
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getLicensesTable();
|
|
|
|
|
$rows = $wpdb->get_results(
|
|
|
|
|
"SELECT * FROM {$tableName} ORDER BY created_at DESC",
|
|
|
|
|
ARRAY_A
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$exportData = [];
|
|
|
|
|
foreach ($rows ?: [] as $row) {
|
|
|
|
|
$product = wc_get_product((int) $row['product_id']);
|
|
|
|
|
$customer = get_userdata((int) $row['customer_id']);
|
|
|
|
|
$order = wc_get_order((int) $row['order_id']);
|
|
|
|
|
|
|
|
|
|
$exportData[] = [
|
|
|
|
|
'ID' => $row['id'],
|
|
|
|
|
'License Key' => $row['license_key'],
|
|
|
|
|
'Product' => $product ? $product->get_name() : 'Unknown',
|
|
|
|
|
'Product ID' => $row['product_id'],
|
|
|
|
|
'Order ID' => $row['order_id'],
|
|
|
|
|
'Order Number' => $order ? $order->get_order_number() : '',
|
|
|
|
|
'Customer' => $customer ? $customer->display_name : 'Guest',
|
|
|
|
|
'Customer Email' => $customer ? $customer->user_email : '',
|
|
|
|
|
'Customer ID' => $row['customer_id'],
|
|
|
|
|
'Domain' => $row['domain'],
|
|
|
|
|
'Status' => ucfirst($row['status']),
|
|
|
|
|
'Activations' => $row['activations_count'],
|
|
|
|
|
'Max Activations' => $row['max_activations'],
|
|
|
|
|
'Expires At' => $row['expires_at'] ?: 'Lifetime',
|
|
|
|
|
'Created At' => $row['created_at'],
|
|
|
|
|
'Updated At' => $row['updated_at'],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $exportData;
|
|
|
|
|
}
|
2026-01-21 18:55:18 +01:00
|
|
|
}
|