feat: Implement OAuth license activation flow
- Add LicenseConnect.tsx focused OAuth confirmation page in customer SPA - Add /licenses/oauth/validate and /licenses/oauth/confirm API endpoints - Update App.tsx to render license-connect outside BaseLayout (no header/footer) - Add license_activation_method field to product settings in Admin SPA - Create LICENSING_MODULE.md with comprehensive OAuth flow documentation - Update API_ROUTES.md with license module endpoints
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* License Manager
|
||||
*
|
||||
@@ -13,53 +14,57 @@ if (!defined('ABSPATH')) exit;
|
||||
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
|
||||
class LicenseManager {
|
||||
|
||||
class LicenseManager
|
||||
{
|
||||
|
||||
private static $table_name = 'woonoow_licenses';
|
||||
private static $activations_table = 'woonoow_license_activations';
|
||||
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
public static function init() {
|
||||
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) {
|
||||
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) {
|
||||
private static function is_virtual_order($order)
|
||||
{
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if ($product && !$product->is_virtual()) {
|
||||
@@ -68,19 +73,20 @@ class LicenseManager {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
public static function create_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,
|
||||
@@ -102,9 +108,9 @@ class LicenseManager {
|
||||
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,
|
||||
@@ -120,44 +126,45 @@ class LicenseManager {
|
||||
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) {
|
||||
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++) {
|
||||
@@ -172,29 +179,31 @@ class LicenseManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if license already exists for order item
|
||||
*/
|
||||
public static function license_exists_for_order_item($order_item_id) {
|
||||
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) {
|
||||
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'],
|
||||
@@ -205,24 +214,25 @@ class LicenseManager {
|
||||
'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() {
|
||||
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();
|
||||
@@ -241,80 +251,84 @@ class LicenseManager {
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return $prefix . $key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get license by key
|
||||
*/
|
||||
public static function get_license_by_key($license_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) {
|
||||
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 = []) {
|
||||
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 = []) {
|
||||
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);
|
||||
@@ -322,22 +336,52 @@ class LicenseManager {
|
||||
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'));
|
||||
}
|
||||
|
||||
|
||||
// Check if product requires OAuth activation
|
||||
// Get licensing module settings
|
||||
$licensing_settings = get_option('woonoow_module_licensing_settings', []);
|
||||
|
||||
// 1. Get site-level setting (default)
|
||||
$activation_method = $licensing_settings['activation_method'] ?? 'api';
|
||||
|
||||
// 2. Check for product-level override (only if allow_product_override is enabled)
|
||||
$allow_override = $licensing_settings['allow_product_override'] ?? false;
|
||||
if ($allow_override) {
|
||||
$product_method = get_post_meta($license['product_id'], '_woonoow_license_activation_method', true);
|
||||
if (!empty($product_method)) {
|
||||
$activation_method = $product_method;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activation_method === 'oauth') {
|
||||
// Check if this is an OAuth callback (has valid activation token)
|
||||
if (!empty($activation_data['activation_token'])) {
|
||||
$validated = self::validate_activation_token($activation_data['activation_token'], $license_key);
|
||||
if (is_wp_error($validated)) {
|
||||
return $validated;
|
||||
}
|
||||
// Token is valid, proceed with activation
|
||||
} else {
|
||||
// Not a callback, return redirect URL for OAuth flow
|
||||
return self::build_oauth_redirect_response($license, $activation_data);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -346,48 +390,186 @@ class LicenseManager {
|
||||
'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
|
||||
'activations_remaining' => $license['activation_limit'] > 0
|
||||
? max(0, $license['activation_limit'] - $license['activation_count'] - 1)
|
||||
: -1,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deactivate license
|
||||
* Build OAuth redirect response for license activation
|
||||
*/
|
||||
public static function deactivate($license_key, $activation_id = null, $machine_id = null) {
|
||||
private static function build_oauth_redirect_response($license, $activation_data)
|
||||
{
|
||||
// Generate state token for CSRF protection
|
||||
$state = self::generate_oauth_state($license['license_key'], $activation_data['domain'] ?? '');
|
||||
|
||||
// Build redirect URL to vendor site
|
||||
$connect_url = home_url('/my-account/license-connect/');
|
||||
$redirect_url = add_query_arg([
|
||||
'license_key' => $license['license_key'],
|
||||
'site_url' => $activation_data['domain'] ?? '',
|
||||
'return_url' => $activation_data['return_url'] ?? '',
|
||||
'state' => $state,
|
||||
'nonce' => wp_create_nonce('woonoow_oauth_connect'),
|
||||
], $connect_url);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'oauth_required',
|
||||
'message' => __('This license requires account verification. You will be redirected to complete activation.', 'woonoow'),
|
||||
'redirect_url' => $redirect_url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OAuth state token
|
||||
*/
|
||||
public static function generate_oauth_state($license_key, $domain)
|
||||
{
|
||||
$data = [
|
||||
'license_key' => $license_key,
|
||||
'domain' => $domain,
|
||||
'timestamp' => time(),
|
||||
];
|
||||
$payload = base64_encode(wp_json_encode($data));
|
||||
$signature = hash_hmac('sha256', $payload, wp_salt('auth'));
|
||||
|
||||
return $payload . '.' . $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify OAuth state token
|
||||
*/
|
||||
public static function verify_oauth_state($state)
|
||||
{
|
||||
$parts = explode('.', $state, 2);
|
||||
if (count($parts) !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($payload, $signature) = $parts;
|
||||
$expected_signature = hash_hmac('sha256', $payload, wp_salt('auth'));
|
||||
|
||||
if (!hash_equals($expected_signature, $signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode(base64_decode($payload), true);
|
||||
if (!$data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check timestamp (10 minute expiry)
|
||||
if (empty($data['timestamp']) || (time() - $data['timestamp']) > 600) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate activation token (short-lived, single-use)
|
||||
*/
|
||||
public static function generate_activation_token($license_id, $domain)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$token = wp_generate_password(32, false);
|
||||
$expires_at = gmdate('Y-m-d H:i:s', time() + 300); // 5 minute expiry
|
||||
|
||||
// Store token in activations table temporarily
|
||||
$table = $wpdb->prefix . self::$activations_table;
|
||||
$wpdb->insert($table, [
|
||||
'license_id' => $license_id,
|
||||
'domain' => $domain,
|
||||
'machine_id' => 'oauth_token:' . $token,
|
||||
'status' => 'pending',
|
||||
'user_agent' => 'OAuth activation token expires: ' . $expires_at,
|
||||
]);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'activation_id' => $wpdb->insert_id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate activation token
|
||||
*/
|
||||
private static function validate_activation_token($token, $license_key)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$license = self::get_license_by_key($license_key);
|
||||
|
||||
if (!$license) {
|
||||
return new \WP_Error('invalid_license', __('Invalid license key', 'woonoow'));
|
||||
}
|
||||
|
||||
|
||||
$table = $wpdb->prefix . self::$activations_table;
|
||||
$activation = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table WHERE license_id = %d AND machine_id = %s AND status = 'pending' LIMIT 1",
|
||||
$license['id'],
|
||||
'oauth_token:' . $token
|
||||
), ARRAY_A);
|
||||
|
||||
if (!$activation) {
|
||||
return new \WP_Error('invalid_token', __('Invalid or expired activation token', 'woonoow'));
|
||||
}
|
||||
|
||||
// Check expiry from user_agent field
|
||||
if (preg_match('/expires: (.+)$/', $activation['user_agent'], $matches)) {
|
||||
$expires_at = strtotime($matches[1]);
|
||||
if ($expires_at && time() > $expires_at) {
|
||||
// Delete expired token
|
||||
$wpdb->delete($table, ['id' => $activation['id']]);
|
||||
return new \WP_Error('token_expired', __('Activation token has expired', 'woonoow'));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the pending record (it will be replaced by actual activation)
|
||||
$wpdb->delete($table, ['id' => $activation['id']]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -395,40 +577,41 @@ class LicenseManager {
|
||||
$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) {
|
||||
public static function validate($license_key)
|
||||
{
|
||||
$license = self::get_license_by_key($license_key);
|
||||
|
||||
|
||||
if (!$license) {
|
||||
return [
|
||||
'valid' => false,
|
||||
@@ -436,20 +619,20 @@ class LicenseManager {
|
||||
'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
|
||||
'activations_remaining' => $license['activation_limit'] > 0
|
||||
? max(0, $license['activation_limit'] - $license['activation_count'])
|
||||
: -1,
|
||||
'expires_at' => $license['expires_at'],
|
||||
@@ -458,76 +641,79 @@ class LicenseManager {
|
||||
'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) {
|
||||
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) {
|
||||
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 = []) {
|
||||
public static function get_all_licenses($args = [])
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
|
||||
$defaults = [
|
||||
'search' => '',
|
||||
'status' => null,
|
||||
@@ -539,56 +725,57 @@ class LicenseManager {
|
||||
'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) {
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Licensing Module Bootstrap
|
||||
*
|
||||
@@ -12,79 +13,306 @@ if (!defined('ABSPATH')) exit;
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
use WooNooW\Modules\LicensingSettings;
|
||||
|
||||
class LicensingModule {
|
||||
|
||||
class LicensingModule
|
||||
{
|
||||
|
||||
/**
|
||||
* Initialize the licensing module
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
// Register settings schema
|
||||
LicensingSettings::init();
|
||||
|
||||
|
||||
// Initialize license manager immediately since we're already in plugins_loaded
|
||||
// Note: This is called from woonoow.php inside plugins_loaded action,
|
||||
// so we can call maybe_init_manager directly instead of scheduling another hook
|
||||
self::maybe_init_manager();
|
||||
|
||||
|
||||
// Install tables on module enable
|
||||
add_action('woonoow/module/enabled', [__CLASS__, 'on_module_enabled']);
|
||||
|
||||
|
||||
// Add product meta fields
|
||||
add_action('woocommerce_product_options_general_product_data', [__CLASS__, 'add_product_licensing_fields']);
|
||||
add_action('woocommerce_process_product_meta', [__CLASS__, 'save_product_licensing_fields']);
|
||||
|
||||
// License Connect OAuth endpoint
|
||||
add_action('init', [__CLASS__, 'register_license_connect_endpoint']);
|
||||
add_action('template_redirect', [__CLASS__, 'handle_license_connect'], 5);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize manager if module is enabled
|
||||
*/
|
||||
public static function maybe_init_manager() {
|
||||
public static function maybe_init_manager()
|
||||
{
|
||||
if (ModuleRegistry::is_enabled('licensing')) {
|
||||
// Ensure tables exist
|
||||
self::ensure_tables();
|
||||
LicenseManager::init();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure database tables exist
|
||||
*/
|
||||
private static function ensure_tables() {
|
||||
private static function ensure_tables()
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_licenses';
|
||||
|
||||
|
||||
// Check if table exists
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
|
||||
LicenseManager::create_tables();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle module enable
|
||||
*/
|
||||
public static function on_module_enabled($module_id) {
|
||||
public static function on_module_enabled($module_id)
|
||||
{
|
||||
if ($module_id === 'licensing') {
|
||||
LicenseManager::create_tables();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register license connect rewrite endpoint
|
||||
*/
|
||||
public static function register_license_connect_endpoint()
|
||||
{
|
||||
add_rewrite_endpoint('license-connect', EP_ROOT | EP_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle license-connect endpoint (OAuth confirmation page)
|
||||
*/
|
||||
public static function handle_license_connect()
|
||||
{
|
||||
// Parse the request URI to check if this is the license-connect page
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$parsed_path = parse_url($request_uri, PHP_URL_PATH);
|
||||
|
||||
// Check if path contains license-connect
|
||||
if (strpos($parsed_path, '/license-connect') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$license_key = sanitize_text_field($_GET['license_key'] ?? '');
|
||||
$site_url = esc_url_raw($_GET['site_url'] ?? '');
|
||||
$return_url = esc_url_raw($_GET['return_url'] ?? '');
|
||||
$state = sanitize_text_field($_GET['state'] ?? '');
|
||||
$action = sanitize_text_field($_GET['action'] ?? '');
|
||||
|
||||
// Handle form submission (confirmation)
|
||||
if ($action === 'confirm' && !empty($_POST['confirm_license'])) {
|
||||
self::process_license_confirmation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Require login
|
||||
if (!is_user_logged_in()) {
|
||||
$login_url = wp_login_url(add_query_arg($_GET, home_url('/my-account/license-connect/')));
|
||||
wp_redirect($login_url);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate parameters
|
||||
if (empty($license_key) || empty($site_url) || empty($state)) {
|
||||
self::render_license_connect_page([
|
||||
'error' => __('Invalid license connection request. Missing required parameters.', 'woonoow'),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify state token
|
||||
$state_data = LicenseManager::verify_oauth_state($state);
|
||||
if (!$state_data) {
|
||||
self::render_license_connect_page([
|
||||
'error' => __('Invalid or expired connection request. Please try again.', 'woonoow'),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get license and verify ownership
|
||||
$license = LicenseManager::get_license_by_key($license_key);
|
||||
if (!$license) {
|
||||
self::render_license_connect_page([
|
||||
'error' => __('License key not found.', 'woonoow'),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify license belongs to current user
|
||||
$current_user_id = get_current_user_id();
|
||||
if ((int)$license['user_id'] !== $current_user_id) {
|
||||
self::render_license_connect_page([
|
||||
'error' => __('This license does not belong to your account.', 'woonoow'),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check license status
|
||||
if ($license['status'] !== 'active') {
|
||||
self::render_license_connect_page([
|
||||
'error' => __('This license is not active.', 'woonoow'),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check activation limit
|
||||
if ($license['activation_limit'] > 0 && $license['activation_count'] >= $license['activation_limit']) {
|
||||
self::render_license_connect_page([
|
||||
'error' => sprintf(
|
||||
__('Activation limit reached (%d/%d sites).', 'woonoow'),
|
||||
$license['activation_count'],
|
||||
$license['activation_limit']
|
||||
),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get product info
|
||||
$product = wc_get_product($license['product_id']);
|
||||
$product_name = $product ? $product->get_name() : __('Unknown Product', 'woonoow');
|
||||
|
||||
// Render confirmation page
|
||||
self::render_license_connect_page([
|
||||
'license' => $license,
|
||||
'product_name' => $product_name,
|
||||
'site_url' => $site_url,
|
||||
'return_url' => $return_url,
|
||||
'state' => $state,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process license confirmation form submission
|
||||
*/
|
||||
private static function process_license_confirmation()
|
||||
{
|
||||
if (!is_user_logged_in()) {
|
||||
wp_die(__('You must be logged in.', 'woonoow'));
|
||||
}
|
||||
|
||||
// Verify nonce
|
||||
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'woonoow_license_connect')) {
|
||||
wp_die(__('Security check failed.', 'woonoow'));
|
||||
}
|
||||
|
||||
$license_key = sanitize_text_field($_POST['license_key'] ?? '');
|
||||
$site_url = esc_url_raw($_POST['site_url'] ?? '');
|
||||
$return_url = esc_url_raw($_POST['return_url'] ?? '');
|
||||
$state = sanitize_text_field($_POST['state'] ?? '');
|
||||
|
||||
// Verify state
|
||||
$state_data = LicenseManager::verify_oauth_state($state);
|
||||
if (!$state_data) {
|
||||
wp_die(__('Invalid or expired request.', 'woonoow'));
|
||||
}
|
||||
|
||||
// Get and verify license
|
||||
$license = LicenseManager::get_license_by_key($license_key);
|
||||
if (!$license || (int)$license['user_id'] !== get_current_user_id()) {
|
||||
wp_die(__('Invalid license.', 'woonoow'));
|
||||
}
|
||||
|
||||
// Generate activation token
|
||||
$token_data = LicenseManager::generate_activation_token($license['id'], $site_url);
|
||||
|
||||
// Build return URL with token
|
||||
$callback_url = add_query_arg([
|
||||
'activation_token' => $token_data['token'],
|
||||
'license_key' => $license_key,
|
||||
'state' => $state,
|
||||
], $return_url);
|
||||
|
||||
// Redirect back to client site
|
||||
wp_redirect($callback_url);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render license connect confirmation page
|
||||
*/
|
||||
private static function render_license_connect_page($args)
|
||||
{
|
||||
// Set headers
|
||||
status_header(200);
|
||||
nocache_headers();
|
||||
|
||||
// Include WP header
|
||||
get_header('woonoow');
|
||||
|
||||
echo '<div class="woonoow-license-connect" style="max-width: 600px; margin: 40px auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', sans-serif;">';
|
||||
|
||||
if (!empty($args['error'])) {
|
||||
echo '<div style="background: #fee; border: 1px solid #c00; padding: 15px; border-radius: 4px; margin-bottom: 20px;">';
|
||||
echo '<strong>' . esc_html__('Error', 'woonoow') . ':</strong> ' . esc_html($args['error']);
|
||||
echo '</div>';
|
||||
echo '<a href="' . esc_url(home_url()) . '" style="color: #0073aa;">← ' . esc_html__('Return Home', 'woonoow') . '</a>';
|
||||
} else {
|
||||
$license = $args['license'];
|
||||
$activations_remaining = $license['activation_limit'] > 0
|
||||
? $license['activation_limit'] - $license['activation_count']
|
||||
: '∞';
|
||||
|
||||
echo '<h1 style="font-size: 24px; margin-bottom: 20px;">' . esc_html__('Connect Site to License', 'woonoow') . '</h1>';
|
||||
|
||||
echo '<div style="background: #f8f9fa; border: 1px solid #e5e7eb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">';
|
||||
echo '<table style="width: 100%; border-collapse: collapse;">';
|
||||
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Site', 'woonoow') . ':</td><td style="padding: 8px 0; font-weight: 600;">' . esc_html($args['site_url']) . '</td></tr>';
|
||||
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Product', 'woonoow') . ':</td><td style="padding: 8px 0; font-weight: 600;">' . esc_html($args['product_name']) . '</td></tr>';
|
||||
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('License', 'woonoow') . ':</td><td style="padding: 8px 0; font-family: monospace;">' . esc_html($license['license_key']) . '</td></tr>';
|
||||
echo '<tr><td style="padding: 8px 0; color: #666;">' . esc_html__('Activations', 'woonoow') . ':</td><td style="padding: 8px 0;">' . esc_html($license['activation_count']) . '/' . ($license['activation_limit'] ?: '∞') . ' ' . esc_html__('used', 'woonoow') . '</td></tr>';
|
||||
echo '</table>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<form method="post" action="' . esc_url(add_query_arg('action', 'confirm', home_url('/my-account/license-connect/'))) . '">';
|
||||
echo wp_nonce_field('woonoow_license_connect', '_wpnonce', true, false);
|
||||
echo '<input type="hidden" name="license_key" value="' . esc_attr($license['license_key']) . '">';
|
||||
echo '<input type="hidden" name="site_url" value="' . esc_attr($args['site_url']) . '">';
|
||||
echo '<input type="hidden" name="return_url" value="' . esc_attr($args['return_url']) . '">';
|
||||
echo '<input type="hidden" name="state" value="' . esc_attr($args['state']) . '">';
|
||||
|
||||
echo '<div style="display: flex; gap: 10px;">';
|
||||
echo '<button type="submit" name="confirm_license" value="1" style="background: #2563eb; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer;">';
|
||||
echo esc_html__('Connect This Site', 'woonoow');
|
||||
echo '</button>';
|
||||
echo '<a href="' . esc_url($args['return_url'] ?: home_url()) . '" style="background: #e5e7eb; color: #374151; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; text-decoration: none;">';
|
||||
echo esc_html__('Cancel', 'woonoow');
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
get_footer('woonoow');
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add licensing fields to product edit page
|
||||
*/
|
||||
public static function add_product_licensing_fields() {
|
||||
public static function add_product_licensing_fields()
|
||||
{
|
||||
global $post;
|
||||
|
||||
|
||||
if (!ModuleRegistry::is_enabled('licensing')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
echo '<div class="options_group show_if_simple show_if_downloadable">';
|
||||
|
||||
|
||||
woocommerce_wp_checkbox([
|
||||
'id' => '_woonoow_licensing_enabled',
|
||||
'label' => __('Enable Licensing', 'woonoow'),
|
||||
'description' => __('Generate license keys for this product on purchase', 'woonoow'),
|
||||
]);
|
||||
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_woonoow_license_activation_limit',
|
||||
'label' => __('Activation Limit', 'woonoow'),
|
||||
@@ -95,7 +323,7 @@ class LicensingModule {
|
||||
'step' => '1',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_woonoow_license_expiry_days',
|
||||
'label' => __('License Expiry (Days)', 'woonoow'),
|
||||
@@ -106,23 +334,48 @@ class LicensingModule {
|
||||
'step' => '1',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// Only show activation method if per-product override is enabled
|
||||
$licensing_settings = get_option('woonoow_module_licensing_settings', []);
|
||||
$allow_override = $licensing_settings['allow_product_override'] ?? false;
|
||||
if ($allow_override) {
|
||||
woocommerce_wp_select([
|
||||
'id' => '_woonoow_license_activation_method',
|
||||
'label' => __('Activation Method', 'woonoow'),
|
||||
'description' => __('Override site-level setting for this product', 'woonoow'),
|
||||
'options' => [
|
||||
'' => __('Use Site Default', 'woonoow'),
|
||||
'api' => __('Simple API (license key only)', 'woonoow'),
|
||||
'oauth' => __('Secure OAuth (requires account login)', 'woonoow'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save licensing fields
|
||||
*/
|
||||
public static function save_product_licensing_fields($post_id) {
|
||||
public static function save_product_licensing_fields($post_id)
|
||||
{
|
||||
$licensing_enabled = isset($_POST['_woonoow_licensing_enabled']) ? 'yes' : 'no';
|
||||
update_post_meta($post_id, '_woonoow_licensing_enabled', $licensing_enabled);
|
||||
|
||||
|
||||
if (isset($_POST['_woonoow_license_activation_limit'])) {
|
||||
update_post_meta($post_id, '_woonoow_license_activation_limit', absint($_POST['_woonoow_license_activation_limit']));
|
||||
}
|
||||
|
||||
|
||||
if (isset($_POST['_woonoow_license_expiry_days'])) {
|
||||
update_post_meta($post_id, '_woonoow_license_expiry_days', absint($_POST['_woonoow_license_expiry_days']));
|
||||
}
|
||||
|
||||
if (isset($_POST['_woonoow_license_activation_method'])) {
|
||||
$method = $_POST['_woonoow_license_activation_method'];
|
||||
// Accept empty (site default), api, or oauth
|
||||
if ($method === '' || in_array($method, ['api', 'oauth'])) {
|
||||
update_post_meta($post_id, '_woonoow_license_activation_method', sanitize_key($method));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user