🔧 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;
 | 
						|
    }
 | 
						|
} |