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')); } // 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, '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, ]; } /** * Build OAuth redirect response for license activation */ 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; } 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); } }