598 lines
21 KiB
PHP
598 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* License Manager
|
|
*
|
|
* Handles license key generation, activation, deactivation, and validation.
|
|
*
|
|
* @package WooNooW\Modules\Licensing
|
|
*/
|
|
|
|
namespace WooNooW\Modules\Licensing;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
use WooNooW\Core\ModuleRegistry;
|
|
|
|
class LicenseManager {
|
|
|
|
private static $table_name = 'woonoow_licenses';
|
|
private static $activations_table = 'woonoow_license_activations';
|
|
|
|
/**
|
|
* Initialize
|
|
*/
|
|
public static function init() {
|
|
// Only initialize if module is enabled
|
|
if (!ModuleRegistry::is_enabled('licensing')) {
|
|
return;
|
|
}
|
|
|
|
// Hook into order completion - multiple hooks to catch all scenarios
|
|
add_action('woocommerce_order_status_completed', [__CLASS__, 'generate_licenses_for_order']);
|
|
add_action('woocommerce_order_status_processing', [__CLASS__, 'generate_licenses_for_order']);
|
|
add_action('woocommerce_payment_complete', [__CLASS__, 'generate_licenses_for_order']);
|
|
|
|
// Also hook into thank you page for COD/pending orders (with lower priority)
|
|
add_action('woocommerce_thankyou', [__CLASS__, 'maybe_generate_on_thankyou'], 10);
|
|
}
|
|
|
|
/**
|
|
* Maybe generate licenses on thank you page (for COD and pending orders)
|
|
*/
|
|
public static function maybe_generate_on_thankyou($order_id) {
|
|
if (!$order_id) return;
|
|
|
|
$order = wc_get_order($order_id);
|
|
if (!$order) return;
|
|
|
|
// Only generate for orders that didn't already get licenses via status hooks
|
|
// Check if it's a virtual-only order that might skip payment completion
|
|
$needs_payment = $order->needs_payment();
|
|
$is_virtual = self::is_virtual_order($order);
|
|
|
|
// Generate if: virtual order OR already paid (processing/completed)
|
|
if ($is_virtual || in_array($order->get_status(), ['processing', 'completed'])) {
|
|
self::generate_licenses_for_order($order_id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if order contains only virtual items
|
|
*/
|
|
private static function is_virtual_order($order) {
|
|
foreach ($order->get_items() as $item) {
|
|
$product = $item->get_product();
|
|
if ($product && !$product->is_virtual()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create database tables
|
|
*/
|
|
public static function create_tables() {
|
|
global $wpdb;
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
$licenses_table = $wpdb->prefix . self::$table_name;
|
|
$activations_table = $wpdb->prefix . self::$activations_table;
|
|
|
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
|
|
// Create licenses table - dbDelta requires each CREATE TABLE to be called separately
|
|
$sql_licenses = "CREATE TABLE $licenses_table (
|
|
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
license_key varchar(255) NOT NULL,
|
|
product_id bigint(20) UNSIGNED NOT NULL,
|
|
order_id bigint(20) UNSIGNED NOT NULL,
|
|
order_item_id bigint(20) UNSIGNED NOT NULL,
|
|
user_id bigint(20) UNSIGNED NOT NULL,
|
|
status varchar(20) NOT NULL DEFAULT 'active',
|
|
activation_limit int(11) NOT NULL DEFAULT 1,
|
|
activation_count int(11) NOT NULL DEFAULT 0,
|
|
expires_at datetime DEFAULT NULL,
|
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY license_key (license_key),
|
|
KEY product_id (product_id),
|
|
KEY order_id (order_id),
|
|
KEY user_id (user_id),
|
|
KEY status (status)
|
|
) $charset_collate;";
|
|
|
|
dbDelta($sql_licenses);
|
|
|
|
// Create activations table
|
|
$sql_activations = "CREATE TABLE $activations_table (
|
|
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
license_id bigint(20) UNSIGNED NOT NULL,
|
|
domain varchar(255) DEFAULT NULL,
|
|
ip_address varchar(45) DEFAULT NULL,
|
|
machine_id varchar(255) DEFAULT NULL,
|
|
user_agent text DEFAULT NULL,
|
|
status varchar(20) NOT NULL DEFAULT 'active',
|
|
activated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
deactivated_at datetime DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY license_id (license_id),
|
|
KEY status (status)
|
|
) $charset_collate;";
|
|
|
|
dbDelta($sql_activations);
|
|
}
|
|
|
|
/**
|
|
* Generate licenses for completed order
|
|
*/
|
|
public static function generate_licenses_for_order($order_id) {
|
|
$order = wc_get_order($order_id);
|
|
if (!$order) return;
|
|
|
|
foreach ($order->get_items() as $item_id => $item) {
|
|
$product_id = $item->get_product_id();
|
|
$product = wc_get_product($product_id);
|
|
|
|
if (!$product) continue;
|
|
|
|
// Check if product has licensing enabled
|
|
$licensing_enabled = get_post_meta($product_id, '_woonoow_licensing_enabled', true);
|
|
if ($licensing_enabled !== 'yes') continue;
|
|
|
|
// Check if license already exists for this order item
|
|
if (self::license_exists_for_order_item($item_id)) continue;
|
|
|
|
// Get activation limit from product or default
|
|
$activation_limit = (int) get_post_meta($product_id, '_woonoow_license_activation_limit', true);
|
|
if ($activation_limit <= 0) {
|
|
$activation_limit = (int) get_option('woonoow_licensing_default_activation_limit', 1);
|
|
}
|
|
|
|
// Get expiry from product or default
|
|
$expiry_days = (int) get_post_meta($product_id, '_woonoow_license_expiry_days', true);
|
|
if ($expiry_days <= 0 && get_option('woonoow_licensing_license_expiry_enabled', false)) {
|
|
$expiry_days = (int) get_option('woonoow_licensing_default_expiry_days', 365);
|
|
}
|
|
|
|
$expires_at = $expiry_days > 0 ? gmdate('Y-m-d H:i:s', strtotime("+$expiry_days days")) : null;
|
|
|
|
// Generate license for each quantity
|
|
$quantity = $item->get_quantity();
|
|
for ($i = 0; $i < $quantity; $i++) {
|
|
self::create_license([
|
|
'product_id' => $product_id,
|
|
'order_id' => $order_id,
|
|
'order_item_id' => $item_id,
|
|
'user_id' => $order->get_user_id(),
|
|
'activation_limit' => $activation_limit,
|
|
'expires_at' => $expires_at,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if license already exists for order item
|
|
*/
|
|
public static function license_exists_for_order_item($order_item_id) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
return (bool) $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM $table WHERE order_item_id = %d",
|
|
$order_item_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Create a new license
|
|
*/
|
|
public static function create_license($data) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
$license_key = self::generate_license_key();
|
|
|
|
$wpdb->insert($table, [
|
|
'license_key' => $license_key,
|
|
'product_id' => $data['product_id'],
|
|
'order_id' => $data['order_id'],
|
|
'order_item_id' => $data['order_item_id'],
|
|
'user_id' => $data['user_id'],
|
|
'activation_limit' => $data['activation_limit'] ?? 1,
|
|
'expires_at' => $data['expires_at'] ?? null,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$license_id = $wpdb->insert_id;
|
|
|
|
do_action('woonoow/license/created', $license_id, $license_key, $data);
|
|
|
|
return [
|
|
'id' => $license_id,
|
|
'license_key' => $license_key,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate license key
|
|
*/
|
|
public static function generate_license_key() {
|
|
$format = get_option('woonoow_licensing_license_key_format', 'serial');
|
|
$prefix = get_option('woonoow_licensing_license_key_prefix', '');
|
|
|
|
switch ($format) {
|
|
case 'uuid':
|
|
$key = wp_generate_uuid4();
|
|
break;
|
|
case 'alphanumeric':
|
|
$key = strtoupper(wp_generate_password(16, false));
|
|
break;
|
|
case 'serial':
|
|
default:
|
|
$key = strtoupper(sprintf(
|
|
'%s-%s-%s-%s',
|
|
wp_generate_password(4, false),
|
|
wp_generate_password(4, false),
|
|
wp_generate_password(4, false),
|
|
wp_generate_password(4, false)
|
|
));
|
|
break;
|
|
}
|
|
|
|
return $prefix . $key;
|
|
}
|
|
|
|
/**
|
|
* Get license by key
|
|
*/
|
|
public static function get_license_by_key($license_key) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE license_key = %s",
|
|
$license_key
|
|
), ARRAY_A);
|
|
}
|
|
|
|
/**
|
|
* Get license by ID
|
|
*/
|
|
public static function get_license($license_id) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE id = %d",
|
|
$license_id
|
|
), ARRAY_A);
|
|
}
|
|
|
|
/**
|
|
* Get licenses for user
|
|
*/
|
|
public static function get_user_licenses($user_id, $args = []) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
$defaults = [
|
|
'status' => null,
|
|
'limit' => 50,
|
|
'offset' => 0,
|
|
];
|
|
$args = wp_parse_args($args, $defaults);
|
|
|
|
$where = "user_id = %d";
|
|
$params = [$user_id];
|
|
|
|
if ($args['status']) {
|
|
$where .= " AND status = %s";
|
|
$params[] = $args['status'];
|
|
}
|
|
|
|
$sql = "SELECT * FROM $table WHERE $where ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
|
$params[] = $args['limit'];
|
|
$params[] = $args['offset'];
|
|
|
|
return $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
|
}
|
|
|
|
/**
|
|
* Activate license
|
|
*/
|
|
public static function activate($license_key, $activation_data = []) {
|
|
global $wpdb;
|
|
$license = self::get_license_by_key($license_key);
|
|
|
|
if (!$license) {
|
|
return new \WP_Error('invalid_license', __('Invalid license key', 'woonoow'));
|
|
}
|
|
|
|
if ($license['status'] !== 'active') {
|
|
return new \WP_Error('license_inactive', __('License is not active', 'woonoow'));
|
|
}
|
|
|
|
// Check expiry
|
|
if ($license['expires_at'] && strtotime($license['expires_at']) < time()) {
|
|
$block_expired = get_option('woonoow_licensing_block_expired_activations', true);
|
|
if ($block_expired) {
|
|
return new \WP_Error('license_expired', __('License has expired', 'woonoow'));
|
|
}
|
|
}
|
|
|
|
// Check subscription status if linked
|
|
$subscription_status = self::get_order_subscription_status($license['order_id']);
|
|
if ($subscription_status !== null && !in_array($subscription_status, ['active', 'pending-cancel'])) {
|
|
return new \WP_Error('subscription_inactive', __('Subscription is not active', 'woonoow'));
|
|
}
|
|
|
|
// Check activation limit
|
|
if ($license['activation_limit'] > 0 && $license['activation_count'] >= $license['activation_limit']) {
|
|
return new \WP_Error('activation_limit_reached', __('Activation limit reached', 'woonoow'));
|
|
}
|
|
|
|
// Create activation record
|
|
$activations_table = $wpdb->prefix . self::$activations_table;
|
|
$licenses_table = $wpdb->prefix . self::$table_name;
|
|
|
|
$wpdb->insert($activations_table, [
|
|
'license_id' => $license['id'],
|
|
'domain' => $activation_data['domain'] ?? null,
|
|
'ip_address' => $activation_data['ip_address'] ?? null,
|
|
'machine_id' => $activation_data['machine_id'] ?? null,
|
|
'user_agent' => $activation_data['user_agent'] ?? null,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Increment activation count
|
|
$wpdb->query($wpdb->prepare(
|
|
"UPDATE $licenses_table SET activation_count = activation_count + 1 WHERE id = %d",
|
|
$license['id']
|
|
));
|
|
|
|
do_action('woonoow/license/activated', $license['id'], $activation_data);
|
|
|
|
return [
|
|
'success' => true,
|
|
'activation_id' => $wpdb->insert_id,
|
|
'activations_remaining' => $license['activation_limit'] > 0
|
|
? max(0, $license['activation_limit'] - $license['activation_count'] - 1)
|
|
: -1,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Deactivate license
|
|
*/
|
|
public static function deactivate($license_key, $activation_id = null, $machine_id = null) {
|
|
global $wpdb;
|
|
$license = self::get_license_by_key($license_key);
|
|
|
|
if (!$license) {
|
|
return new \WP_Error('invalid_license', __('Invalid license key', 'woonoow'));
|
|
}
|
|
|
|
// Check if deactivation is allowed
|
|
$allow_deactivation = get_option('woonoow_licensing_allow_deactivation', true);
|
|
if (!$allow_deactivation) {
|
|
return new \WP_Error('deactivation_disabled', __('License deactivation is disabled', 'woonoow'));
|
|
}
|
|
|
|
$activations_table = $wpdb->prefix . self::$activations_table;
|
|
$licenses_table = $wpdb->prefix . self::$table_name;
|
|
|
|
// Find activation to deactivate
|
|
$where = "license_id = %d AND status = 'active'";
|
|
$params = [$license['id']];
|
|
|
|
if ($activation_id) {
|
|
$where .= " AND id = %d";
|
|
$params[] = $activation_id;
|
|
} elseif ($machine_id) {
|
|
$where .= " AND machine_id = %s";
|
|
$params[] = $machine_id;
|
|
}
|
|
|
|
$activation = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $activations_table WHERE $where LIMIT 1",
|
|
$params
|
|
), ARRAY_A);
|
|
|
|
if (!$activation) {
|
|
return new \WP_Error('no_activation', __('No active activation found', 'woonoow'));
|
|
}
|
|
|
|
// Deactivate
|
|
$wpdb->update(
|
|
$activations_table,
|
|
['status' => 'deactivated', 'deactivated_at' => current_time('mysql')],
|
|
['id' => $activation['id']]
|
|
);
|
|
|
|
// Decrement activation count
|
|
$wpdb->query($wpdb->prepare(
|
|
"UPDATE $licenses_table SET activation_count = GREATEST(0, activation_count - 1) WHERE id = %d",
|
|
$license['id']
|
|
));
|
|
|
|
do_action('woonoow/license/deactivated', $license['id'], $activation['id']);
|
|
|
|
return ['success' => true];
|
|
}
|
|
|
|
/**
|
|
* Validate license (check if valid without activating)
|
|
*/
|
|
public static function validate($license_key) {
|
|
$license = self::get_license_by_key($license_key);
|
|
|
|
if (!$license) {
|
|
return [
|
|
'valid' => false,
|
|
'error' => 'invalid_license',
|
|
'message' => __('Invalid license key', 'woonoow'),
|
|
];
|
|
}
|
|
|
|
$is_expired = $license['expires_at'] && strtotime($license['expires_at']) < time();
|
|
|
|
// Check subscription status if linked
|
|
$subscription_status = self::get_order_subscription_status($license['order_id']);
|
|
$is_subscription_valid = $subscription_status === null || in_array($subscription_status, ['active', 'pending-cancel']);
|
|
|
|
return [
|
|
'valid' => $license['status'] === 'active' && !$is_expired && $is_subscription_valid,
|
|
'license_key' => $license['license_key'],
|
|
'status' => $license['status'],
|
|
'activation_limit' => (int) $license['activation_limit'],
|
|
'activation_count' => (int) $license['activation_count'],
|
|
'activations_remaining' => $license['activation_limit'] > 0
|
|
? max(0, $license['activation_limit'] - $license['activation_count'])
|
|
: -1,
|
|
'expires_at' => $license['expires_at'],
|
|
'is_expired' => $is_expired,
|
|
'subscription_status' => $subscription_status,
|
|
'subscription_active' => $is_subscription_valid,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if an order has a linked subscription and return its status
|
|
*
|
|
* @param int $order_id
|
|
* @return string|null Subscription status or null if no subscription
|
|
*/
|
|
public static function get_order_subscription_status($order_id) {
|
|
// Check if subscription module is enabled
|
|
if (!ModuleRegistry::is_enabled('subscription')) {
|
|
return null;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . 'woonoow_subscription_orders';
|
|
|
|
// Check if table exists
|
|
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table'");
|
|
if (!$table_exists) {
|
|
return null;
|
|
}
|
|
|
|
// Find subscription linked to this order
|
|
$subscription_id = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT subscription_id FROM $table WHERE order_id = %d LIMIT 1",
|
|
$order_id
|
|
));
|
|
|
|
if (!$subscription_id) {
|
|
return null;
|
|
}
|
|
|
|
// Get subscription status
|
|
$subscriptions_table = $wpdb->prefix . 'woonoow_subscriptions';
|
|
$status = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT status FROM $subscriptions_table WHERE id = %d",
|
|
$subscription_id
|
|
));
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Revoke license
|
|
*/
|
|
public static function revoke($license_id) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
$result = $wpdb->update(
|
|
$table,
|
|
['status' => 'revoked'],
|
|
['id' => $license_id]
|
|
);
|
|
|
|
if ($result !== false) {
|
|
do_action('woonoow/license/revoked', $license_id);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get all licenses (admin)
|
|
*/
|
|
public static function get_all_licenses($args = []) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$table_name;
|
|
|
|
$defaults = [
|
|
'search' => '',
|
|
'status' => null,
|
|
'product_id' => null,
|
|
'user_id' => null,
|
|
'limit' => 50,
|
|
'offset' => 0,
|
|
'orderby' => 'created_at',
|
|
'order' => 'DESC',
|
|
];
|
|
$args = wp_parse_args($args, $defaults);
|
|
|
|
$where_clauses = ['1=1'];
|
|
$params = [];
|
|
|
|
if ($args['search']) {
|
|
$where_clauses[] = "license_key LIKE %s";
|
|
$params[] = '%' . $wpdb->esc_like($args['search']) . '%';
|
|
}
|
|
|
|
if ($args['status']) {
|
|
$where_clauses[] = "status = %s";
|
|
$params[] = $args['status'];
|
|
}
|
|
|
|
if ($args['product_id']) {
|
|
$where_clauses[] = "product_id = %d";
|
|
$params[] = $args['product_id'];
|
|
}
|
|
|
|
if ($args['user_id']) {
|
|
$where_clauses[] = "user_id = %d";
|
|
$params[] = $args['user_id'];
|
|
}
|
|
|
|
$where = implode(' AND ', $where_clauses);
|
|
$orderby = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']) ?: 'created_at DESC';
|
|
|
|
$sql = "SELECT * FROM $table WHERE $where ORDER BY $orderby LIMIT %d OFFSET %d";
|
|
$params[] = $args['limit'];
|
|
$params[] = $args['offset'];
|
|
|
|
$licenses = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
|
|
|
// Get total count
|
|
$count_sql = "SELECT COUNT(*) FROM $table WHERE $where";
|
|
$total = $wpdb->get_var($wpdb->prepare($count_sql, array_slice($params, 0, -2)));
|
|
|
|
return [
|
|
'licenses' => $licenses,
|
|
'total' => (int) $total,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get activations for a license
|
|
*/
|
|
public static function get_activations($license_id) {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::$activations_table;
|
|
|
|
return $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM $table WHERE license_id = %d ORDER BY activated_at DESC",
|
|
$license_id
|
|
), ARRAY_A);
|
|
}
|
|
}
|