🔧 Bug Fixes: - Fixed download limits defaulting to 5 instead of 0 for unlimited downloads - Fixed software license filename sanitization (spaces→dashes, dots→underscores, proper .zip extension) - Software downloads now show as "Test-Plugin-v2-2-0.zip" instead of "Test Plugin v2.2.0" ✨ UI/UX Enhancements: - Redesigned license key display to span full table width with FontAwesome copy icons - Added responsive CSS styling for license key rows - Integrated FontAwesome CDN for modern copy icons 🏗️ Architecture Improvements: - Added comprehensive filename sanitization in both download handler and API paths - Enhanced software license product handling for local package files - Improved error handling and logging throughout download processes 📦 Infrastructure: - Added Gitea workflows for automated releases on push to main - Created comprehensive .gitignore excluding test files and browser automation - Updated documentation with all recent improvements and technical insights 🔍 Technical Details: - Software license products served from wp-content/uploads/wpdd-packages/ - Download flow: token → process_download_by_token() → process_download() → deliver_file() - Dual path coverage for both API downloads and regular file delivery - Version placeholder system for automated deployment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
424 lines
14 KiB
PHP
424 lines
14 KiB
PHP
<?php
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class WPDD_License_Manager {
|
|
|
|
/**
|
|
* Initialize the license manager
|
|
*/
|
|
public static function init() {
|
|
add_action('wpdd_order_completed', array(__CLASS__, 'generate_license_for_order'), 10, 1);
|
|
}
|
|
|
|
/**
|
|
* Generate a unique license key
|
|
* Format: XXXX-XXXX-XXXX-XXXX
|
|
*/
|
|
public static function generate_license_key() {
|
|
$segments = array();
|
|
for ($i = 0; $i < 4; $i++) {
|
|
$segments[] = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
|
|
}
|
|
return implode('-', $segments);
|
|
}
|
|
|
|
/**
|
|
* Generate license for completed order
|
|
*/
|
|
public static function generate_license_for_order($order_id) {
|
|
global $wpdb;
|
|
|
|
error_log('WPDD License Debug: generate_license_for_order called for order ID: ' . $order_id);
|
|
|
|
// Get the order details
|
|
$order = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d",
|
|
$order_id
|
|
));
|
|
|
|
if (!$order) {
|
|
error_log('WPDD License Debug: Order not found for ID: ' . $order_id);
|
|
return;
|
|
}
|
|
|
|
// Check if product is software license type
|
|
$product_type = get_post_meta($order->product_id, '_wpdd_product_type', true);
|
|
error_log('WPDD License Debug: Product type for product ' . $order->product_id . ': ' . $product_type);
|
|
|
|
if ($product_type !== 'software_license') {
|
|
error_log('WPDD License Debug: Product type is not software_license, skipping license generation');
|
|
return;
|
|
}
|
|
|
|
// Check if license already exists for this order
|
|
$existing = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT id FROM {$wpdb->prefix}wpdd_licenses WHERE order_id = %d",
|
|
$order_id
|
|
));
|
|
|
|
if ($existing) {
|
|
error_log('WPDD License Debug: License already exists for order ' . $order_id . ', license ID: ' . $existing);
|
|
return;
|
|
}
|
|
|
|
error_log('WPDD License Debug: Generating new license for order ' . $order_id);
|
|
|
|
// Generate unique license key
|
|
$license_key = self::generate_license_key();
|
|
|
|
// Ensure it's unique
|
|
while (self::license_key_exists($license_key)) {
|
|
$license_key = self::generate_license_key();
|
|
}
|
|
|
|
// Get license settings from product
|
|
$max_activations = get_post_meta($order->product_id, '_wpdd_max_activations', true) ?: 1;
|
|
$license_duration = get_post_meta($order->product_id, '_wpdd_license_duration', true); // in days
|
|
|
|
$expires_at = null;
|
|
if ($license_duration && $license_duration > 0) {
|
|
$expires_at = date('Y-m-d H:i:s', strtotime("+{$license_duration} days"));
|
|
}
|
|
|
|
// Insert license
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'wpdd_licenses',
|
|
array(
|
|
'license_key' => $license_key,
|
|
'product_id' => $order->product_id,
|
|
'order_id' => $order_id,
|
|
'customer_id' => $order->customer_id,
|
|
'customer_email' => $order->customer_email,
|
|
'status' => 'active',
|
|
'max_activations' => $max_activations,
|
|
'expires_at' => $expires_at,
|
|
'created_at' => current_time('mysql')
|
|
),
|
|
array('%s', '%d', '%d', '%d', '%s', '%s', '%d', '%s', '%s')
|
|
);
|
|
|
|
if ($wpdb->insert_id) {
|
|
error_log('WPDD License Debug: License created successfully with ID: ' . $wpdb->insert_id . ', license key: ' . $license_key);
|
|
} else {
|
|
error_log('WPDD License Debug: Failed to create license. Last error: ' . $wpdb->last_error);
|
|
}
|
|
|
|
// Send license key to customer
|
|
self::send_license_email($order, $license_key);
|
|
|
|
return $license_key;
|
|
}
|
|
|
|
/**
|
|
* Check if license key exists
|
|
*/
|
|
public static function license_key_exists($license_key) {
|
|
global $wpdb;
|
|
return $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s",
|
|
$license_key
|
|
)) > 0;
|
|
}
|
|
|
|
/**
|
|
* Validate license key
|
|
*/
|
|
public static function validate_license($license_key, $product_slug = null, $site_url = null) {
|
|
global $wpdb;
|
|
|
|
// Get license details
|
|
$license = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT l.*, p.post_name as product_slug
|
|
FROM {$wpdb->prefix}wpdd_licenses l
|
|
LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID
|
|
WHERE l.license_key = %s",
|
|
$license_key
|
|
));
|
|
|
|
if (!$license) {
|
|
return array(
|
|
'valid' => false,
|
|
'error' => 'invalid_license',
|
|
'message' => __('Invalid license key.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
// Check product match
|
|
if ($product_slug && $license->product_slug !== $product_slug) {
|
|
return array(
|
|
'valid' => false,
|
|
'error' => 'product_mismatch',
|
|
'message' => __('License key is not valid for this product.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
// Check status
|
|
if ($license->status !== 'active') {
|
|
return array(
|
|
'valid' => false,
|
|
'error' => 'license_' . $license->status,
|
|
'message' => sprintf(__('License is %s.', 'wp-digital-download'), $license->status)
|
|
);
|
|
}
|
|
|
|
// Check expiration
|
|
if ($license->expires_at && strtotime($license->expires_at) < time()) {
|
|
// Update status to expired
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'wpdd_licenses',
|
|
array('status' => 'expired'),
|
|
array('id' => $license->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
|
|
return array(
|
|
'valid' => false,
|
|
'error' => 'license_expired',
|
|
'message' => __('License has expired.', 'wp-digital-download'),
|
|
'expired_at' => $license->expires_at
|
|
);
|
|
}
|
|
|
|
// Check activation limit if site_url provided
|
|
if ($site_url) {
|
|
$activation_count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d AND status = 'active'",
|
|
$license->id
|
|
));
|
|
|
|
$is_activated = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d AND site_url = %s AND status = 'active'",
|
|
$license->id,
|
|
$site_url
|
|
));
|
|
|
|
if (!$is_activated && $activation_count >= $license->max_activations) {
|
|
return array(
|
|
'valid' => false,
|
|
'error' => 'activation_limit',
|
|
'message' => sprintf(__('License activation limit reached (%d/%d).', 'wp-digital-download'),
|
|
$activation_count, $license->max_activations),
|
|
'activations' => $activation_count,
|
|
'max_activations' => $license->max_activations
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update last checked
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'wpdd_licenses',
|
|
array('last_checked' => current_time('mysql')),
|
|
array('id' => $license->id),
|
|
array('%s'),
|
|
array('%d')
|
|
);
|
|
|
|
return array(
|
|
'valid' => true,
|
|
'license' => $license,
|
|
'message' => __('License is valid.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Activate license for a site
|
|
*/
|
|
public static function activate_license($license_key, $site_url, $site_name = null, $wp_version = null, $php_version = null) {
|
|
global $wpdb;
|
|
|
|
// Validate license first
|
|
$validation = self::validate_license($license_key, null, $site_url);
|
|
if (!$validation['valid']) {
|
|
return $validation;
|
|
}
|
|
|
|
$license = $validation['license'];
|
|
|
|
// Check if already activated for this site
|
|
$existing = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d AND site_url = %s",
|
|
$license->id,
|
|
$site_url
|
|
));
|
|
|
|
if ($existing && $existing->status === 'active') {
|
|
return array(
|
|
'success' => true,
|
|
'already_active' => true,
|
|
'message' => __('License already activated for this site.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
if ($existing) {
|
|
// Reactivate
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'wpdd_license_activations',
|
|
array(
|
|
'status' => 'active',
|
|
'activated_at' => current_time('mysql'),
|
|
'last_checked' => current_time('mysql'),
|
|
'wp_version' => $wp_version,
|
|
'php_version' => $php_version,
|
|
'site_name' => $site_name
|
|
),
|
|
array('id' => $existing->id),
|
|
array('%s', '%s', '%s', '%s', '%s', '%s'),
|
|
array('%d')
|
|
);
|
|
} else {
|
|
// New activation
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'wpdd_license_activations',
|
|
array(
|
|
'license_id' => $license->id,
|
|
'license_key' => $license_key,
|
|
'site_url' => $site_url,
|
|
'site_name' => $site_name,
|
|
'activated_at' => current_time('mysql'),
|
|
'last_checked' => current_time('mysql'),
|
|
'wp_version' => $wp_version,
|
|
'php_version' => $php_version,
|
|
'status' => 'active'
|
|
),
|
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')
|
|
);
|
|
}
|
|
|
|
// Update activation count
|
|
$count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d AND status = 'active'",
|
|
$license->id
|
|
));
|
|
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'wpdd_licenses',
|
|
array('activations_count' => $count),
|
|
array('id' => $license->id),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
|
|
return array(
|
|
'success' => true,
|
|
'message' => __('License activated successfully.', 'wp-digital-download'),
|
|
'activations' => $count,
|
|
'max_activations' => $license->max_activations
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deactivate license for a site
|
|
*/
|
|
public static function deactivate_license($license_key, $site_url) {
|
|
global $wpdb;
|
|
|
|
// Get license
|
|
$license = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s",
|
|
$license_key
|
|
));
|
|
|
|
if (!$license) {
|
|
return array(
|
|
'success' => false,
|
|
'error' => 'invalid_license',
|
|
'message' => __('Invalid license key.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
// Deactivate
|
|
$updated = $wpdb->update(
|
|
$wpdb->prefix . 'wpdd_license_activations',
|
|
array('status' => 'deactivated'),
|
|
array(
|
|
'license_id' => $license->id,
|
|
'site_url' => $site_url
|
|
),
|
|
array('%s'),
|
|
array('%d', '%s')
|
|
);
|
|
|
|
if (!$updated) {
|
|
return array(
|
|
'success' => false,
|
|
'error' => 'not_activated',
|
|
'message' => __('License not activated for this site.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
// Update activation count
|
|
$count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d AND status = 'active'",
|
|
$license->id
|
|
));
|
|
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'wpdd_licenses',
|
|
array('activations_count' => $count),
|
|
array('id' => $license->id),
|
|
array('%d'),
|
|
array('%d')
|
|
);
|
|
|
|
return array(
|
|
'success' => true,
|
|
'message' => __('License deactivated successfully.', 'wp-digital-download')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send license email to customer
|
|
*/
|
|
private static function send_license_email($order, $license_key) {
|
|
$product = get_post($order->product_id);
|
|
$subject = sprintf(__('Your License Key for %s', 'wp-digital-download'), $product->post_title);
|
|
|
|
$message = sprintf(
|
|
__("Hi %s,\n\nThank you for your purchase!\n\nHere is your license key for %s:\n\n%s\n\nPlease keep this key safe. You will need it to activate and receive updates for your software.\n\nBest regards,\n%s", 'wp-digital-download'),
|
|
$order->customer_name ?: $order->customer_email,
|
|
$product->post_title,
|
|
$license_key,
|
|
get_bloginfo('name')
|
|
);
|
|
|
|
wp_mail($order->customer_email, $subject, $message);
|
|
}
|
|
|
|
/**
|
|
* Get license details for admin
|
|
*/
|
|
public static function get_license_details($license_key) {
|
|
global $wpdb;
|
|
|
|
$license = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT l.*, p.post_title as product_name
|
|
FROM {$wpdb->prefix}wpdd_licenses l
|
|
LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID
|
|
WHERE l.license_key = %s",
|
|
$license_key
|
|
));
|
|
|
|
if (!$license) {
|
|
return null;
|
|
}
|
|
|
|
// Get activations
|
|
$license->activations = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}wpdd_license_activations
|
|
WHERE license_id = %d
|
|
ORDER BY activated_at DESC",
|
|
$license->id
|
|
));
|
|
|
|
return $license;
|
|
}
|
|
} |