🔧 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>
		
			
				
	
	
		
			818 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			818 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
if (!defined('ABSPATH')) {
 | 
						|
    exit;
 | 
						|
}
 | 
						|
 | 
						|
class WPDD_Download_Handler {
 | 
						|
    
 | 
						|
    public static function init() {
 | 
						|
        // Use priority 1 to ensure our handler runs early
 | 
						|
        add_action('init', array(__CLASS__, 'handle_download_request'), 1);
 | 
						|
        add_action('init', array(__CLASS__, 'handle_secure_file_download'), 1);
 | 
						|
        add_action('init', array(__CLASS__, 'handle_protected_download'), 1);
 | 
						|
        
 | 
						|
        // Also hook to template_redirect as a fallback
 | 
						|
        add_action('template_redirect', array(__CLASS__, 'handle_download_request'), 1);
 | 
						|
        
 | 
						|
        // Debug: Add a test endpoint for admins
 | 
						|
        if (current_user_can('manage_options') && isset($_GET['wpdd_test_download'])) {
 | 
						|
            add_action('init', function() {
 | 
						|
                wp_die('WPDD Download Handler is loaded and working! Current time: ' . current_time('mysql'));
 | 
						|
            }, 0);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function handle_download_request() {
 | 
						|
        // TESTING: Add a test parameter to verify changes are taking effect
 | 
						|
        if (isset($_GET['test_update_working'])) {
 | 
						|
            wp_die('TEST: Changes are working! File updated successfully.');
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Early exit if not a download request
 | 
						|
        if (!isset($_GET['wpdd_download']) && !isset($_GET['wpdd_download_token'])) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Debug logging for admin users
 | 
						|
        if (current_user_can('manage_options')) {
 | 
						|
            error_log('WPDD Debug: Download request detected!');
 | 
						|
            error_log('WPDD Debug: GET params: ' . print_r($_GET, true));
 | 
						|
            error_log('WPDD Debug: Current action: ' . current_action());
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (isset($_GET['wpdd_download'])) {
 | 
						|
            // Add debug output before processing
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                error_log('WPDD Debug: Processing download by order ID: ' . $_GET['wpdd_download']);
 | 
						|
            }
 | 
						|
            self::process_download_by_order();
 | 
						|
            exit; // Make sure we exit after processing
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (isset($_GET['wpdd_download_token'])) {
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                error_log('WPDD Debug: Processing download by token: ' . $_GET['wpdd_download_token']);
 | 
						|
            }
 | 
						|
            self::process_download_by_token();
 | 
						|
            exit; // Make sure we exit after processing
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function handle_protected_download() {
 | 
						|
        if (!isset($_GET['wpdd_protected_download'])) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $file_id = sanitize_text_field($_GET['wpdd_protected_download']);
 | 
						|
        $token = sanitize_text_field($_GET['token'] ?? '');
 | 
						|
        
 | 
						|
        if (!$file_id || !$token) {
 | 
						|
            wp_die(__('Invalid download link.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get file metadata
 | 
						|
        $file_meta = get_option('wpdd_protected_file_' . $file_id);
 | 
						|
        
 | 
						|
        if (!$file_meta || $file_meta['token'] !== $token) {
 | 
						|
            wp_die(__('Invalid download token.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!file_exists($file_meta['file_path'])) {
 | 
						|
            wp_die(__('File not found.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Deliver the file
 | 
						|
        self::deliver_protected_file($file_meta['file_path']);
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function process_download_by_order() {
 | 
						|
        $order_id = intval($_GET['wpdd_download']);
 | 
						|
        
 | 
						|
        // Debug nonce verification
 | 
						|
        if (current_user_can('manage_options')) {
 | 
						|
            error_log('WPDD Debug: Order ID: ' . $order_id);
 | 
						|
            error_log('WPDD Debug: Nonce received: ' . ($_GET['_wpnonce'] ?? 'none'));
 | 
						|
            error_log('WPDD Debug: Expected nonce action: wpdd_download_' . $order_id);
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wpdd_download_' . $order_id)) {
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                error_log('WPDD Debug: Nonce verification failed!');
 | 
						|
            }
 | 
						|
            wp_die(__('Invalid download link.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Check by email if guest
 | 
						|
        if (isset($_GET['customer_email']) && isset($_GET['key'])) {
 | 
						|
            $email = sanitize_email($_GET['customer_email']);
 | 
						|
            $key = sanitize_text_field($_GET['key']);
 | 
						|
            
 | 
						|
            // Verify the key
 | 
						|
            $expected_key = substr(md5($email . AUTH_KEY), 0, 10);
 | 
						|
            if ($key !== $expected_key) {
 | 
						|
                wp_die(__('Invalid access key.', 'wp-digital-download'));
 | 
						|
            }
 | 
						|
            
 | 
						|
            $order = $wpdb->get_row($wpdb->prepare(
 | 
						|
                "SELECT * FROM {$wpdb->prefix}wpdd_orders 
 | 
						|
                 WHERE id = %d AND customer_email = %s AND status = 'completed'",
 | 
						|
                $order_id,
 | 
						|
                $email
 | 
						|
            ));
 | 
						|
        } elseif (is_user_logged_in()) {
 | 
						|
            $current_user = wp_get_current_user();
 | 
						|
            
 | 
						|
            $order = $wpdb->get_row($wpdb->prepare(
 | 
						|
                "SELECT * FROM {$wpdb->prefix}wpdd_orders 
 | 
						|
                 WHERE id = %d AND (customer_id = %d OR customer_email = %s) AND status = 'completed'",
 | 
						|
                $order_id,
 | 
						|
                $current_user->ID,
 | 
						|
                $current_user->user_email
 | 
						|
            ));
 | 
						|
        } else {
 | 
						|
            // For unregistered users, try to look up order by order number from URL if available
 | 
						|
            if (isset($_GET['order_id'])) {
 | 
						|
                $order_number = sanitize_text_field($_GET['order_id']);
 | 
						|
                
 | 
						|
                // Look up order by order ID and verify it matches the order number
 | 
						|
                $order = $wpdb->get_row($wpdb->prepare(
 | 
						|
                    "SELECT * FROM {$wpdb->prefix}wpdd_orders 
 | 
						|
                     WHERE id = %d AND order_number = %s AND status = 'completed'",
 | 
						|
                    $order_id,
 | 
						|
                    $order_number
 | 
						|
                ));
 | 
						|
                
 | 
						|
                if (current_user_can('manage_options')) {
 | 
						|
                    error_log('WPDD Debug: Guest order lookup - Order ID: ' . $order_id . ', Order Number: ' . $order_number . ', Found: ' . ($order ? 'Yes' : 'No'));
 | 
						|
                }
 | 
						|
                
 | 
						|
                if (!$order) {
 | 
						|
                    wp_die(__('Invalid order or order not found.', 'wp-digital-download'));
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                // Debug: Show what parameters we have
 | 
						|
                $debug_info = '';
 | 
						|
                if (current_user_can('manage_options')) {
 | 
						|
                    $debug_info = '<br><br>Debug info:<br>';
 | 
						|
                    $debug_info .= 'GET params: ' . print_r($_GET, true);
 | 
						|
                    $debug_info .= '<br>User logged in: ' . (is_user_logged_in() ? 'Yes' : 'No');
 | 
						|
                }
 | 
						|
                wp_die(__('You must be logged in to download this product or provide a valid order reference.', 'wp-digital-download') . $debug_info);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!$order) {
 | 
						|
            wp_die(__('Invalid order or you do not have permission to download this product.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        self::process_download($order);
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function process_download_by_token() {
 | 
						|
        $token = sanitize_text_field($_GET['wpdd_download_token']);
 | 
						|
        
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        $download_link = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT dl.*, o.* 
 | 
						|
             FROM {$wpdb->prefix}wpdd_download_links dl
 | 
						|
             INNER JOIN {$wpdb->prefix}wpdd_orders o ON dl.order_id = o.id
 | 
						|
             WHERE dl.token = %s",
 | 
						|
            $token
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$download_link) {
 | 
						|
            wp_die(__('Invalid download link.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        if ($download_link->expires_at < current_time('mysql')) {
 | 
						|
            // Check if user still has downloads remaining
 | 
						|
            if ($download_link->max_downloads > 0 && $download_link->download_count >= $download_link->max_downloads) {
 | 
						|
                wp_die(__('This download link has expired and you have no downloads remaining.', 'wp-digital-download'));
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Only send refresh email if this appears to be a real user attempt (not automated)
 | 
						|
            // Check if the customer is unregistered (has no user account)
 | 
						|
            $is_unregistered_customer = ($download_link->customer_id == 0);
 | 
						|
            
 | 
						|
            if ($is_unregistered_customer) {
 | 
						|
                // Generate new token and send refresh email only for unregistered customers
 | 
						|
                $new_token = self::refresh_download_token($download_link->order_id, $download_link->customer_email);
 | 
						|
                
 | 
						|
                if ($new_token) {
 | 
						|
                    wp_die(sprintf(
 | 
						|
                        __('Your download link has expired. A new download link has been sent to %s. Please check your email and try again.', 'wp-digital-download'),
 | 
						|
                        esc_html($download_link->customer_email)
 | 
						|
                    ));
 | 
						|
                } else {
 | 
						|
                    wp_die(__('This download link has expired and could not be refreshed. Please contact support.', 'wp-digital-download'));
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                // For registered users, just show expired message (they can log in to get new links)
 | 
						|
                wp_die(__('This download link has expired. Please log in to your account to get a new download link.', 'wp-digital-download'));
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if ($download_link->max_downloads > 0 && $download_link->download_count >= $download_link->max_downloads) {
 | 
						|
            wp_die(__('Download limit exceeded.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        $wpdb->update(
 | 
						|
            $wpdb->prefix . 'wpdd_download_links',
 | 
						|
            array('download_count' => $download_link->download_count + 1),
 | 
						|
            array('id' => $download_link->id),
 | 
						|
            array('%d'),
 | 
						|
            array('%d')
 | 
						|
        );
 | 
						|
        
 | 
						|
        self::process_download($download_link);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Refresh an expired download token and send new link via email
 | 
						|
     */
 | 
						|
    private static function refresh_download_token($order_id, $customer_email) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Generate new token with extended expiry (72 hours as suggested)
 | 
						|
        $new_token = wp_hash(uniqid() . $order_id . time());
 | 
						|
        $new_expires_at = date('Y-m-d H:i:s', strtotime('+72 hours'));
 | 
						|
        
 | 
						|
        // Update the existing download link with new token and expiry
 | 
						|
        $updated = $wpdb->update(
 | 
						|
            $wpdb->prefix . 'wpdd_download_links',
 | 
						|
            array(
 | 
						|
                'token' => $new_token,
 | 
						|
                'expires_at' => $new_expires_at,
 | 
						|
                'refreshed_at' => current_time('mysql')
 | 
						|
            ),
 | 
						|
            array('order_id' => $order_id),
 | 
						|
            array('%s', '%s', '%s'),
 | 
						|
            array('%d')
 | 
						|
        );
 | 
						|
        
 | 
						|
        if (!$updated) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Send refresh email
 | 
						|
        self::send_refresh_email($order_id, $new_token, $customer_email);
 | 
						|
        
 | 
						|
        return $new_token;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Send refresh email with new download link
 | 
						|
     */
 | 
						|
    private static function send_refresh_email($order_id, $token, $customer_email) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Get order details
 | 
						|
        $order = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT o.*, p.post_title as product_name 
 | 
						|
             FROM {$wpdb->prefix}wpdd_orders o
 | 
						|
             LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
 | 
						|
             WHERE o.id = %d",
 | 
						|
            $order_id
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$order) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $download_url = add_query_arg(array(
 | 
						|
            'wpdd_download_token' => $token
 | 
						|
        ), home_url());
 | 
						|
        
 | 
						|
        $subject = sprintf(__('New Download Link for %s', 'wp-digital-download'), $order->product_name);
 | 
						|
        
 | 
						|
        $message = sprintf(
 | 
						|
            __("Hello %s,\n\nYour download link for \"%s\" has expired, so we've generated a new one for you.\n\nThis new link is valid for 72 hours and can be used for your remaining downloads.\n\nDownload Link: %s\n\nOrder Number: %s\nPurchase Date: %s\n\nIf you have any issues, please contact our support team.\n\nBest regards,\n%s", 'wp-digital-download'),
 | 
						|
            $order->customer_name,
 | 
						|
            $order->product_name,
 | 
						|
            $download_url,
 | 
						|
            $order->order_number,
 | 
						|
            date_i18n(get_option('date_format'), strtotime($order->purchase_date)),
 | 
						|
            get_bloginfo('name')
 | 
						|
        );
 | 
						|
        
 | 
						|
        $headers = array('Content-Type: text/plain; charset=UTF-8');
 | 
						|
        
 | 
						|
        return wp_mail($customer_email, $subject, $message, $headers);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Create download token for orders that don't have one (legacy orders)
 | 
						|
     */
 | 
						|
    public static function ensure_download_token($order_id) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Check if token already exists
 | 
						|
        $existing_token = $wpdb->get_var($wpdb->prepare(
 | 
						|
            "SELECT token FROM {$wpdb->prefix}wpdd_download_links WHERE order_id = %d",
 | 
						|
            $order_id
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if ($existing_token) {
 | 
						|
            return $existing_token;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Create new token for legacy order
 | 
						|
        $token = wp_hash(uniqid() . $order_id . time());
 | 
						|
        $expires_at = date('Y-m-d H:i:s', strtotime('+7 days'));
 | 
						|
        
 | 
						|
        $wpdb->insert(
 | 
						|
            $wpdb->prefix . 'wpdd_download_links',
 | 
						|
            array(
 | 
						|
                'order_id' => $order_id,
 | 
						|
                'token' => $token,
 | 
						|
                'expires_at' => $expires_at,
 | 
						|
                'max_downloads' => 5,
 | 
						|
                'created_at' => current_time('mysql')
 | 
						|
            ),
 | 
						|
            array('%d', '%s', '%s', '%d', '%s')
 | 
						|
        );
 | 
						|
        
 | 
						|
        return $token;
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function process_download($order) {
 | 
						|
        $product_id = $order->product_id;
 | 
						|
        $files = get_post_meta($product_id, '_wpdd_files', true);
 | 
						|
        
 | 
						|
        // Debug output for admins
 | 
						|
        if (current_user_can('manage_options') && empty($files)) {
 | 
						|
            wp_die(sprintf(__('Debug: No files found for product ID %d. Files data: %s', 'wp-digital-download'), 
 | 
						|
                $product_id, '<pre>' . print_r($files, true) . '</pre>'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (empty($files)) {
 | 
						|
            wp_die(__('No files available for download.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Debug output for admins
 | 
						|
        if (current_user_can('manage_options')) {
 | 
						|
            error_log('WPDD Debug: Files for product ' . $product_id . ': ' . print_r($files, true));
 | 
						|
        }
 | 
						|
        
 | 
						|
        $download_limit = get_post_meta($product_id, '_wpdd_download_limit', true);
 | 
						|
        $download_expiry = get_post_meta($product_id, '_wpdd_download_expiry', true);
 | 
						|
        
 | 
						|
        if ($download_expiry > 0) {
 | 
						|
            $expiry_date = date('Y-m-d H:i:s', strtotime($order->purchase_date . ' + ' . $download_expiry . ' days'));
 | 
						|
            if (current_time('mysql') > $expiry_date) {
 | 
						|
                wp_die(__('Your download period has expired.', 'wp-digital-download'));
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if ($download_limit > 0 && $order->download_count >= $download_limit) {
 | 
						|
            wp_die(__('You have reached the download limit for this product.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        global $wpdb;
 | 
						|
        $wpdb->update(
 | 
						|
            $wpdb->prefix . 'wpdd_orders',
 | 
						|
            array('download_count' => $order->download_count + 1),
 | 
						|
            array('id' => $order->id),
 | 
						|
            array('%d'),
 | 
						|
            array('%d')
 | 
						|
        );
 | 
						|
        
 | 
						|
        $file_index = isset($_GET['file']) ? intval($_GET['file']) : 0;
 | 
						|
        
 | 
						|
        // Handle array structure - files might be indexed or not
 | 
						|
        $file_list = array_values($files); // Reindex to ensure numeric keys
 | 
						|
        
 | 
						|
        if (count($file_list) > 1 && !isset($_GET['file'])) {
 | 
						|
            self::show_file_selection($file_list, $order);
 | 
						|
            exit;
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!isset($file_list[$file_index])) {
 | 
						|
            // Debug output for admins
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                wp_die(sprintf(__('Debug: File index %d not found. Available files: %s', 'wp-digital-download'), 
 | 
						|
                    $file_index, '<pre>' . print_r($file_list, true) . '</pre>'));
 | 
						|
            }
 | 
						|
            wp_die(__('File not found.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        $file = $file_list[$file_index];
 | 
						|
        
 | 
						|
        self::log_download($order, $product_id, $file['id'] ?? $file_index);
 | 
						|
        
 | 
						|
        // Debug for admins
 | 
						|
        if (current_user_can('manage_options')) {
 | 
						|
            error_log('WPDD Debug: Processing file - ID: ' . ($file['id'] ?? 'none') . ', URL: ' . ($file['url'] ?? 'none'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if this is a protected file
 | 
						|
        if (isset($file['id']) && strpos($file['id'], 'wpdd_') === 0) {
 | 
						|
            // This is a protected file, get its metadata
 | 
						|
            $file_meta = get_option('wpdd_protected_file_' . $file['id']);
 | 
						|
            
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                error_log('WPDD Debug: Protected file detected - ' . $file['id']);
 | 
						|
                error_log('WPDD Debug: File meta exists: ' . ($file_meta ? 'Yes' : 'No'));
 | 
						|
                if ($file_meta) {
 | 
						|
                    error_log('WPDD Debug: File path exists: ' . (file_exists($file_meta['file_path']) ? 'Yes' : 'No'));
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            if ($file_meta && file_exists($file_meta['file_path'])) {
 | 
						|
                $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
 | 
						|
                $product_name = get_the_title($product_id);
 | 
						|
                
 | 
						|
                if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) {
 | 
						|
                    $watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order);
 | 
						|
                    if ($watermarked_file) {
 | 
						|
                        self::deliver_protected_file($watermarked_file, true, $product_name);
 | 
						|
                    } else {
 | 
						|
                        self::deliver_protected_file($file_meta['file_path'], false, $product_name);
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    self::deliver_protected_file($file_meta['file_path'], false, $product_name);
 | 
						|
                }
 | 
						|
                return;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if URL contains protected download parameter (alternative check)
 | 
						|
        if (isset($file['url']) && strpos($file['url'], 'wpdd_protected_download=') !== false) {
 | 
						|
            // Extract file_id from URL
 | 
						|
            if (preg_match('/wpdd_protected_download=([^&]+)/', $file['url'], $matches)) {
 | 
						|
                $file_id = $matches[1];
 | 
						|
                $file_meta = get_option('wpdd_protected_file_' . $file_id);
 | 
						|
                
 | 
						|
                if (current_user_can('manage_options')) {
 | 
						|
                    error_log('WPDD Debug: Protected URL detected - ' . $file_id);
 | 
						|
                }
 | 
						|
                
 | 
						|
                if ($file_meta && file_exists($file_meta['file_path'])) {
 | 
						|
                    $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
 | 
						|
                    $product_name = get_the_title($product_id);
 | 
						|
                    
 | 
						|
                    if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) {
 | 
						|
                        $watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order);
 | 
						|
                        if ($watermarked_file) {
 | 
						|
                            self::deliver_protected_file($watermarked_file, true, $product_name);
 | 
						|
                        } else {
 | 
						|
                            self::deliver_protected_file($file_meta['file_path'], false, $product_name);
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        self::deliver_protected_file($file_meta['file_path'], false, $product_name);
 | 
						|
                    }
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Regular file handling (backward compatibility)
 | 
						|
        $enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
 | 
						|
        
 | 
						|
        // Generate proper filename for software license products
 | 
						|
        $product_type = get_post_meta($product_id, '_wpdd_product_type', true);
 | 
						|
        $final_filename = $file['name'];
 | 
						|
        
 | 
						|
        if ($product_type === 'software_license') {
 | 
						|
            // Check if this is a package file (contains version info)
 | 
						|
            if (strpos($file['url'], 'wpdd-packages/') !== false && preg_match('/package-v([^\/]+)\.zip$/', $file['url'], $matches)) {
 | 
						|
                $version = $matches[1];
 | 
						|
                $product_name = get_the_title($product_id);
 | 
						|
                
 | 
						|
                // Create sanitized filename using product name and version
 | 
						|
                $safe_name = str_replace([' ', '.'], ['-', '_'], $product_name . ' v' . $version);
 | 
						|
                $safe_name = sanitize_file_name($safe_name);
 | 
						|
                $final_filename = $safe_name . '.zip';
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if ($enable_watermark && self::is_watermarkable($file['url'])) {
 | 
						|
            $watermarked_file = WPDD_Watermark::apply_watermark($file['url'], $order);
 | 
						|
            if ($watermarked_file) {
 | 
						|
                self::deliver_file($watermarked_file, $final_filename, true);
 | 
						|
            } else {
 | 
						|
                self::deliver_file($file['url'], $final_filename);
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            self::deliver_file($file['url'], $final_filename);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function show_file_selection($files, $order) {
 | 
						|
        ?>
 | 
						|
        <!DOCTYPE html>
 | 
						|
        <html <?php language_attributes(); ?>>
 | 
						|
        <head>
 | 
						|
            <meta charset="<?php bloginfo('charset'); ?>">
 | 
						|
            <title><?php _e('Select File to Download', 'wp-digital-download'); ?></title>
 | 
						|
            <style>
 | 
						|
                body {
 | 
						|
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
 | 
						|
                    max-width: 600px;
 | 
						|
                    margin: 50px auto;
 | 
						|
                    padding: 20px;
 | 
						|
                    background: #f5f5f5;
 | 
						|
                }
 | 
						|
                .container {
 | 
						|
                    background: white;
 | 
						|
                    padding: 30px;
 | 
						|
                    border-radius: 8px;
 | 
						|
                    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
						|
                }
 | 
						|
                h1 {
 | 
						|
                    color: #333;
 | 
						|
                    margin-bottom: 20px;
 | 
						|
                }
 | 
						|
                .file-list {
 | 
						|
                    list-style: none;
 | 
						|
                    padding: 0;
 | 
						|
                }
 | 
						|
                .file-item {
 | 
						|
                    margin-bottom: 15px;
 | 
						|
                    padding: 15px;
 | 
						|
                    background: #f9f9f9;
 | 
						|
                    border-radius: 4px;
 | 
						|
                    display: flex;
 | 
						|
                    justify-content: space-between;
 | 
						|
                    align-items: center;
 | 
						|
                }
 | 
						|
                .file-name {
 | 
						|
                    font-weight: 500;
 | 
						|
                }
 | 
						|
                .download-btn {
 | 
						|
                    padding: 8px 16px;
 | 
						|
                    background: #2271b1;
 | 
						|
                    color: white;
 | 
						|
                    text-decoration: none;
 | 
						|
                    border-radius: 4px;
 | 
						|
                    transition: background 0.2s;
 | 
						|
                }
 | 
						|
                .download-btn:hover {
 | 
						|
                    background: #135e96;
 | 
						|
                }
 | 
						|
            </style>
 | 
						|
        </head>
 | 
						|
        <body>
 | 
						|
            <div class="container">
 | 
						|
                <h1><?php _e('Select File to Download', 'wp-digital-download'); ?></h1>
 | 
						|
                <ul class="file-list">
 | 
						|
                    <?php foreach ($files as $index => $file) : ?>
 | 
						|
                        <li class="file-item">
 | 
						|
                            <span class="file-name"><?php echo esc_html($file['name'] ?? 'File ' . ($index + 1)); ?></span>
 | 
						|
                            <a href="<?php echo add_query_arg('file', $index); ?>" class="download-btn">
 | 
						|
                                <?php _e('Download', 'wp-digital-download'); ?>
 | 
						|
                            </a>
 | 
						|
                        </li>
 | 
						|
                    <?php endforeach; ?>
 | 
						|
                </ul>
 | 
						|
            </div>
 | 
						|
        </body>
 | 
						|
        </html>
 | 
						|
        <?php
 | 
						|
    }
 | 
						|
    
 | 
						|
    public static function handle_secure_file_download() {
 | 
						|
        if (!isset($_GET['wpdd_file_download'])) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $attachment_id = intval($_GET['wpdd_file_download']);
 | 
						|
        $token = sanitize_text_field($_GET['token'] ?? '');
 | 
						|
        
 | 
						|
        if (!$attachment_id || !$token) {
 | 
						|
            wp_die(__('Invalid download link.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Verify token
 | 
						|
        $stored_token = get_post_meta($attachment_id, '_wpdd_download_token', true);
 | 
						|
        if ($token !== $stored_token) {
 | 
						|
            wp_die(__('Invalid download token.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if user has permission to download
 | 
						|
        if (!is_user_logged_in()) {
 | 
						|
            wp_die(__('You must be logged in to download this file.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get protected file path
 | 
						|
        $protected_path = get_post_meta($attachment_id, '_wpdd_protected_path', true);
 | 
						|
        if (!$protected_path || !file_exists($protected_path)) {
 | 
						|
            // Fallback to regular attachment path
 | 
						|
            $protected_path = get_attached_file($attachment_id);
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!$protected_path || !file_exists($protected_path)) {
 | 
						|
            wp_die(__('File not found.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Deliver the file
 | 
						|
        self::deliver_protected_file($protected_path);
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function deliver_protected_file($file_path, $is_temp = false, $product_name = null) {
 | 
						|
        if (!file_exists($file_path)) {
 | 
						|
            wp_die(__('File not found.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        $file_size = filesize($file_path);
 | 
						|
        $file_info = wp_check_filetype($file_path);
 | 
						|
        
 | 
						|
        // If product name is provided, use it with the original extension
 | 
						|
        if ($product_name) {
 | 
						|
            $original_file_name = basename($file_path);
 | 
						|
            $original_extension = pathinfo($original_file_name, PATHINFO_EXTENSION);
 | 
						|
            
 | 
						|
            // If no extension found, try to detect it
 | 
						|
            if (empty($original_extension)) {
 | 
						|
                // Check if it's a zip file by reading the first few bytes
 | 
						|
                $handle = fopen($file_path, 'rb');
 | 
						|
                if ($handle) {
 | 
						|
                    $header = fread($handle, 4);
 | 
						|
                    fclose($handle);
 | 
						|
                    
 | 
						|
                    // ZIP files start with PK (0x504B)
 | 
						|
                    if (substr($header, 0, 2) === 'PK') {
 | 
						|
                        $original_extension = 'zip';
 | 
						|
                        $file_info['type'] = 'application/zip';
 | 
						|
                        $file_info['ext'] = 'zip';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                
 | 
						|
                // If still no extension, try wp_check_filetype result
 | 
						|
                if (empty($original_extension) && !empty($file_info['ext'])) {
 | 
						|
                    $original_extension = $file_info['ext'];
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Sanitize product name for filename use
 | 
						|
            // Convert spaces to dashes and dots to underscores, then use standard sanitization
 | 
						|
            $safe_product_name = str_replace([' ', '.'], ['-', '_'], $product_name);
 | 
						|
            $safe_product_name = sanitize_file_name($safe_product_name);
 | 
						|
            $file_name = $safe_product_name . ($original_extension ? '.' . $original_extension : '');
 | 
						|
        } else {
 | 
						|
            // Fallback to original logic if no product name provided
 | 
						|
            $file_name = basename($file_path);
 | 
						|
            $has_extension = strpos($file_name, '.') !== false;
 | 
						|
            
 | 
						|
            // If no extension in filename, try to determine from content
 | 
						|
            if (!$has_extension) {
 | 
						|
                // Check if it's a zip file by reading the first few bytes
 | 
						|
                $handle = fopen($file_path, 'rb');
 | 
						|
                if ($handle) {
 | 
						|
                    $header = fread($handle, 4);
 | 
						|
                    fclose($handle);
 | 
						|
                    
 | 
						|
                    // ZIP files start with PK (0x504B)
 | 
						|
                    if (substr($header, 0, 2) === 'PK') {
 | 
						|
                        $file_info['type'] = 'application/zip';
 | 
						|
                        $file_info['ext'] = 'zip';
 | 
						|
                        $file_name .= '.zip';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            // If we still don't have an extension but wp_check_filetype detected one, add it
 | 
						|
            if (!$has_extension && !empty($file_info['ext']) && strpos($file_name, '.' . $file_info['ext']) === false) {
 | 
						|
                $file_name .= '.' . $file_info['ext'];
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        nocache_headers();
 | 
						|
        
 | 
						|
        header('Content-Type: ' . ($file_info['type'] ?: 'application/octet-stream'));
 | 
						|
        header('Content-Disposition: attachment; filename="' . $file_name . '"');
 | 
						|
        header('Content-Length: ' . $file_size);
 | 
						|
        header('Content-Transfer-Encoding: binary');
 | 
						|
        
 | 
						|
        if (ob_get_level()) {
 | 
						|
            ob_end_clean();
 | 
						|
        }
 | 
						|
        
 | 
						|
        readfile($file_path);
 | 
						|
        
 | 
						|
        if ($is_temp && file_exists($file_path)) {
 | 
						|
            unlink($file_path);
 | 
						|
        }
 | 
						|
        
 | 
						|
        exit;
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function deliver_file($file_path, $file_name, $is_temp = false) {
 | 
						|
        $original_path = $file_path;
 | 
						|
        
 | 
						|
        // Check if this is a protected file URL
 | 
						|
        if (strpos($file_path, 'wpdd_file_download=') !== false) {
 | 
						|
            // This is already a protected URL, just redirect to it
 | 
						|
            wp_redirect($file_path);
 | 
						|
            exit;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if file is in protected directory
 | 
						|
        $upload_dir = wp_upload_dir();
 | 
						|
        $protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
 | 
						|
        
 | 
						|
        if (strpos($file_path, $protected_dir) === 0) {
 | 
						|
            // File is in protected directory, deliver directly
 | 
						|
            if (!file_exists($file_path)) {
 | 
						|
                wp_die(__('Protected file not found.', 'wp-digital-download'));
 | 
						|
            }
 | 
						|
            
 | 
						|
            self::deliver_protected_file($file_path);
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Convert URL to file path if needed
 | 
						|
        if (filter_var($file_path, FILTER_VALIDATE_URL)) {
 | 
						|
            $upload_dir = wp_upload_dir();
 | 
						|
            $site_url = get_site_url();
 | 
						|
            
 | 
						|
            // Debug logging
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                error_log('WPDD Debug: Original URL: ' . $file_path);
 | 
						|
                error_log('WPDD Debug: Upload baseurl: ' . $upload_dir['baseurl']);
 | 
						|
                error_log('WPDD Debug: Upload basedir: ' . $upload_dir['basedir']);
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Handle various URL formats
 | 
						|
            if (strpos($file_path, $upload_dir['baseurl']) === 0) {
 | 
						|
                $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_path);
 | 
						|
            } elseif (strpos($file_path, $site_url) === 0) {
 | 
						|
                // Handle site URL paths
 | 
						|
                $file_path = str_replace($site_url . '/wp-content/uploads', $upload_dir['basedir'], $file_path);
 | 
						|
            } else {
 | 
						|
                // External URL - just redirect
 | 
						|
                wp_redirect($file_path);
 | 
						|
                exit;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Debug logging
 | 
						|
        if (current_user_can('manage_options')) {
 | 
						|
            error_log('WPDD Debug: Final file path: ' . $file_path);
 | 
						|
            error_log('WPDD Debug: File exists: ' . (file_exists($file_path) ? 'Yes' : 'No'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!file_exists($file_path)) {
 | 
						|
            // Debug output for admin users
 | 
						|
            if (current_user_can('manage_options')) {
 | 
						|
                wp_die(sprintf(__('File not found at path: %s<br>Original: %s', 'wp-digital-download'), 
 | 
						|
                    $file_path, $original_path));
 | 
						|
            }
 | 
						|
            wp_die(__('File not found. Please contact the administrator.', 'wp-digital-download'));
 | 
						|
        }
 | 
						|
        
 | 
						|
        $file_size = filesize($file_path);
 | 
						|
        $file_type = wp_check_filetype($file_path);
 | 
						|
        
 | 
						|
        if (empty($file_name)) {
 | 
						|
            $file_name = basename($file_path);
 | 
						|
        }
 | 
						|
        
 | 
						|
        nocache_headers();
 | 
						|
        
 | 
						|
        header('Content-Type: ' . ($file_type['type'] ?: 'application/octet-stream'));
 | 
						|
        header('Content-Disposition: attachment; filename="' . $file_name . '"');
 | 
						|
        header('Content-Length: ' . $file_size);
 | 
						|
        header('Content-Transfer-Encoding: binary');
 | 
						|
        
 | 
						|
        if (ob_get_level()) {
 | 
						|
            ob_end_clean();
 | 
						|
        }
 | 
						|
        
 | 
						|
        readfile($file_path);
 | 
						|
        
 | 
						|
        if ($is_temp && file_exists($file_path)) {
 | 
						|
            unlink($file_path);
 | 
						|
        }
 | 
						|
        
 | 
						|
        exit;
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function log_download($order, $product_id, $file_id) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        $wpdb->insert(
 | 
						|
            $wpdb->prefix . 'wpdd_downloads',
 | 
						|
            array(
 | 
						|
                'order_id' => $order->id,
 | 
						|
                'product_id' => $product_id,
 | 
						|
                'customer_id' => $order->customer_id,
 | 
						|
                'file_id' => $file_id,
 | 
						|
                'download_date' => current_time('mysql'),
 | 
						|
                'ip_address' => $_SERVER['REMOTE_ADDR'],
 | 
						|
                'user_agent' => $_SERVER['HTTP_USER_AGENT']
 | 
						|
            ),
 | 
						|
            array('%d', '%d', '%d', '%s', '%s', '%s', '%s')
 | 
						|
        );
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static function is_watermarkable($file_url) {
 | 
						|
        $supported_types = array('jpg', 'jpeg', 'png', 'gif', 'pdf');
 | 
						|
        $file_extension = strtolower(pathinfo($file_url, PATHINFO_EXTENSION));
 | 
						|
        return in_array($file_extension, $supported_types);
 | 
						|
    }
 | 
						|
}
 |