🔧 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>
		
			
				
	
	
		
			944 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			944 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
if (!defined('ABSPATH')) {
 | 
						|
    exit;
 | 
						|
}
 | 
						|
 | 
						|
class WPDD_API {
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Initialize the API endpoints
 | 
						|
     */
 | 
						|
    public static function init() {
 | 
						|
        add_action('rest_api_init', array(__CLASS__, 'register_routes'));
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Register REST API routes
 | 
						|
     */
 | 
						|
    public static function register_routes() {
 | 
						|
        // License validation
 | 
						|
        register_rest_route('wpdd/v1', '/validate-license', array(
 | 
						|
            'methods' => 'POST',
 | 
						|
            'callback' => array(__CLASS__, 'validate_license'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'license_key' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'product_slug' => array(
 | 
						|
                    'required' => false,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'site_url' => array(
 | 
						|
                    'required' => false,
 | 
						|
                    'sanitize_callback' => 'esc_url_raw'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
        
 | 
						|
        // License activation
 | 
						|
        register_rest_route('wpdd/v1', '/activate-license', array(
 | 
						|
            'methods' => 'POST',
 | 
						|
            'callback' => array(__CLASS__, 'activate_license'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'license_key' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'site_url' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'esc_url_raw'
 | 
						|
                ),
 | 
						|
                'site_name' => array(
 | 
						|
                    'required' => false,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'wp_version' => array(
 | 
						|
                    'required' => false,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'php_version' => array(
 | 
						|
                    'required' => false,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
        
 | 
						|
        // License deactivation
 | 
						|
        register_rest_route('wpdd/v1', '/deactivate-license', array(
 | 
						|
            'methods' => 'POST',
 | 
						|
            'callback' => array(__CLASS__, 'deactivate_license'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'license_key' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'site_url' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'esc_url_raw'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
        
 | 
						|
        // Check for updates
 | 
						|
        register_rest_route('wpdd/v1', '/check-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
 | 
						|
            'methods' => 'GET',
 | 
						|
            'callback' => array(__CLASS__, 'check_update'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'product_slug' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'license_key' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'version' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
        
 | 
						|
        // Download update
 | 
						|
        register_rest_route('wpdd/v1', '/download-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
 | 
						|
            'methods' => 'GET',
 | 
						|
            'callback' => array(__CLASS__, 'download_update'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'product_slug' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                ),
 | 
						|
                'license_key' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
        
 | 
						|
        // Webhook endpoint with secure passcode
 | 
						|
        register_rest_route('wpdd/v1', '/webhook/(?P<product_id>\d+)/(?P<passcode>[a-zA-Z0-9]+)', array(
 | 
						|
            'methods' => 'POST',
 | 
						|
            'callback' => array(__CLASS__, 'handle_webhook'),
 | 
						|
            'permission_callback' => '__return_true',
 | 
						|
            'args' => array(
 | 
						|
                'product_id' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'absint'
 | 
						|
                ),
 | 
						|
                'passcode' => array(
 | 
						|
                    'required' => true,
 | 
						|
                    'sanitize_callback' => 'sanitize_text_field'
 | 
						|
                )
 | 
						|
            )
 | 
						|
        ));
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Validate license endpoint
 | 
						|
     */
 | 
						|
    public static function validate_license($request) {
 | 
						|
        $license_key = $request->get_param('license_key');
 | 
						|
        $product_slug = $request->get_param('product_slug');
 | 
						|
        $site_url = $request->get_param('site_url');
 | 
						|
        
 | 
						|
        if (!class_exists('WPDD_License_Manager')) {
 | 
						|
            require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
 | 
						|
        }
 | 
						|
        
 | 
						|
        $result = WPDD_License_Manager::validate_license($license_key, $product_slug, $site_url);
 | 
						|
        
 | 
						|
        if ($result['valid']) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => true,
 | 
						|
                'message' => $result['message'],
 | 
						|
                'license' => array(
 | 
						|
                    'status' => $result['license']->status,
 | 
						|
                    'expires_at' => $result['license']->expires_at,
 | 
						|
                    'activations' => $result['license']->activations_count,
 | 
						|
                    'max_activations' => $result['license']->max_activations
 | 
						|
                )
 | 
						|
            ), 200);
 | 
						|
        } else {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => $result['error'],
 | 
						|
                'message' => $result['message']
 | 
						|
            ), 400);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Activate license endpoint
 | 
						|
     */
 | 
						|
    public static function activate_license($request) {
 | 
						|
        $license_key = $request->get_param('license_key');
 | 
						|
        $site_url = $request->get_param('site_url');
 | 
						|
        $site_name = $request->get_param('site_name');
 | 
						|
        $wp_version = $request->get_param('wp_version');
 | 
						|
        $php_version = $request->get_param('php_version');
 | 
						|
        
 | 
						|
        if (!class_exists('WPDD_License_Manager')) {
 | 
						|
            require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
 | 
						|
        }
 | 
						|
        
 | 
						|
        $result = WPDD_License_Manager::activate_license($license_key, $site_url, $site_name, $wp_version, $php_version);
 | 
						|
        
 | 
						|
        if ($result['success']) {
 | 
						|
            return new WP_REST_Response($result, 200);
 | 
						|
        } else {
 | 
						|
            return new WP_REST_Response($result, 400);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Deactivate license endpoint
 | 
						|
     */
 | 
						|
    public static function deactivate_license($request) {
 | 
						|
        $license_key = $request->get_param('license_key');
 | 
						|
        $site_url = $request->get_param('site_url');
 | 
						|
        
 | 
						|
        if (!class_exists('WPDD_License_Manager')) {
 | 
						|
            require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
 | 
						|
        }
 | 
						|
        
 | 
						|
        $result = WPDD_License_Manager::deactivate_license($license_key, $site_url);
 | 
						|
        
 | 
						|
        if ($result['success']) {
 | 
						|
            return new WP_REST_Response($result, 200);
 | 
						|
        } else {
 | 
						|
            return new WP_REST_Response($result, 400);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Check for updates endpoint
 | 
						|
     */
 | 
						|
    public static function check_update($request) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        $product_slug = $request->get_param('product_slug');
 | 
						|
        $license_key = $request->get_param('license_key');
 | 
						|
        $current_version = $request->get_param('version');
 | 
						|
        
 | 
						|
        // Validate license first
 | 
						|
        if (!class_exists('WPDD_License_Manager')) {
 | 
						|
            require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
 | 
						|
        }
 | 
						|
        
 | 
						|
        $validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
 | 
						|
        if (!$validation['valid']) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => $validation['error'],
 | 
						|
                'message' => $validation['message']
 | 
						|
            ), 403);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get product by slug
 | 
						|
        $product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
 | 
						|
        if (!$product) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Product not found.', 'wp-digital-download')
 | 
						|
            ), 404);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get latest version
 | 
						|
        $latest_version = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT * FROM {$wpdb->prefix}wpdd_software_versions 
 | 
						|
             WHERE product_id = %d 
 | 
						|
             ORDER BY release_date DESC 
 | 
						|
             LIMIT 1",
 | 
						|
            $product->ID
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$latest_version) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => true,
 | 
						|
                'update_available' => false,
 | 
						|
                'message' => __('No updates available.', 'wp-digital-download')
 | 
						|
            ), 200);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Compare versions
 | 
						|
        if (version_compare($latest_version->version, $current_version, '>')) {
 | 
						|
            // Update available
 | 
						|
            $update_data = array(
 | 
						|
                'success' => true,
 | 
						|
                'update_available' => true,
 | 
						|
                'version' => $latest_version->version,
 | 
						|
                'download_url' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
 | 
						|
                'package' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
 | 
						|
                'url' => get_permalink($product->ID),
 | 
						|
                'tested' => $latest_version->tested_wp_version ?: get_bloginfo('version'),
 | 
						|
                'requires' => $latest_version->min_wp_version ?: '5.0',
 | 
						|
                'requires_php' => $latest_version->min_php_version ?: '7.0',
 | 
						|
                'new_version' => $latest_version->version,
 | 
						|
                'slug' => $product_slug,
 | 
						|
                'plugin' => $product_slug . '/' . $product_slug . '.php', // Adjust based on your naming convention
 | 
						|
                'changelog' => $latest_version->changelog,
 | 
						|
                'release_notes' => $latest_version->release_notes
 | 
						|
            );
 | 
						|
            
 | 
						|
            return new WP_REST_Response($update_data, 200);
 | 
						|
        } else {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => true,
 | 
						|
                'update_available' => false,
 | 
						|
                'message' => __('You have the latest version.', 'wp-digital-download')
 | 
						|
            ), 200);
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Download update endpoint
 | 
						|
     */
 | 
						|
    public static function download_update($request) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        $product_slug = $request->get_param('product_slug');
 | 
						|
        $license_key = $request->get_param('license_key');
 | 
						|
        
 | 
						|
        // Validate license
 | 
						|
        if (!class_exists('WPDD_License_Manager')) {
 | 
						|
            require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
 | 
						|
        }
 | 
						|
        
 | 
						|
        $validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
 | 
						|
        if (!$validation['valid']) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => $validation['error'],
 | 
						|
                'message' => $validation['message']
 | 
						|
            ), 403);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get product
 | 
						|
        $product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
 | 
						|
        if (!$product) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Product not found.', 'wp-digital-download')
 | 
						|
            ), 404);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get latest version
 | 
						|
        $latest_version = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT * FROM {$wpdb->prefix}wpdd_software_versions 
 | 
						|
             WHERE product_id = %d 
 | 
						|
             ORDER BY release_date DESC 
 | 
						|
             LIMIT 1",
 | 
						|
            $product->ID
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$latest_version || !$latest_version->package_url) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Update package not available.', 'wp-digital-download')
 | 
						|
            ), 404);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get package file path
 | 
						|
        $upload_dir = wp_upload_dir();
 | 
						|
        $package_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $latest_version->package_url);
 | 
						|
        
 | 
						|
        if (!file_exists($package_path)) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Update package file not found.', 'wp-digital-download')
 | 
						|
            ), 404);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Log download
 | 
						|
        $wpdb->insert(
 | 
						|
            $wpdb->prefix . 'wpdd_downloads',
 | 
						|
            array(
 | 
						|
                'order_id' => $validation['license']->order_id,
 | 
						|
                'product_id' => $product->ID,
 | 
						|
                'customer_id' => $validation['license']->customer_id,
 | 
						|
                'file_id' => $latest_version->version,
 | 
						|
                'download_date' => current_time('mysql'),
 | 
						|
                'ip_address' => $_SERVER['REMOTE_ADDR'],
 | 
						|
                'user_agent' => $_SERVER['HTTP_USER_AGENT']
 | 
						|
            ),
 | 
						|
            array('%d', '%d', '%d', '%s', '%s', '%s', '%s')
 | 
						|
        );
 | 
						|
        
 | 
						|
        // Serve file with proper filename
 | 
						|
        $original_filename = basename($package_path);
 | 
						|
        $original_extension = pathinfo($original_filename, PATHINFO_EXTENSION);
 | 
						|
        
 | 
						|
        // If no extension, assume it's a zip file
 | 
						|
        if (empty($original_extension)) {
 | 
						|
            $original_extension = 'zip';
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Create sanitized filename using product name and version
 | 
						|
        $product_name = $product->post_title;
 | 
						|
        $version = $latest_version->version;
 | 
						|
        $safe_name = str_replace([' ', '.'], ['-', '_'], $product_name . ' v' . $version);
 | 
						|
        $safe_name = sanitize_file_name($safe_name);
 | 
						|
        $filename = $safe_name . '.' . $original_extension;
 | 
						|
        
 | 
						|
        header('Content-Type: application/zip');
 | 
						|
        header('Content-Disposition: attachment; filename="' . $filename . '"');
 | 
						|
        header('Content-Length: ' . filesize($package_path));
 | 
						|
        header('Pragma: no-cache');
 | 
						|
        header('Expires: 0');
 | 
						|
        readfile($package_path);
 | 
						|
        exit;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Handle Git webhook for new releases (receives notifications FROM Git platforms like Gitea)
 | 
						|
     */
 | 
						|
    public static function handle_webhook($request) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        $product_id = $request->get_param('product_id');
 | 
						|
        $passcode = $request->get_param('passcode');
 | 
						|
        
 | 
						|
        // Validate passcode
 | 
						|
        $stored_passcode = get_post_meta($product_id, '_wpdd_webhook_passcode', true);
 | 
						|
        if (!$stored_passcode || $stored_passcode !== $passcode) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Invalid webhook passcode.', 'wp-digital-download')
 | 
						|
            ), 403);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get payload from Git platform (Gitea, GitHub, GitLab, etc.)
 | 
						|
        $payload = $request->get_body();
 | 
						|
        $data = json_decode($payload, true);
 | 
						|
        
 | 
						|
        if (!$data) {
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Invalid JSON payload.', 'wp-digital-download')
 | 
						|
            ), 400);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Determine event type based on payload structure
 | 
						|
        $event_type = 'unknown';
 | 
						|
        $is_release = false;
 | 
						|
        
 | 
						|
        // Gitea release webhook
 | 
						|
        if (isset($data['action']) && isset($data['release'])) {
 | 
						|
            $event_type = 'release';
 | 
						|
            $is_release = ($data['action'] === 'published' || $data['action'] === 'created');
 | 
						|
        }
 | 
						|
        // GitHub/GitLab push with tags
 | 
						|
        elseif (isset($data['ref']) && strpos($data['ref'], 'refs/tags/') === 0) {
 | 
						|
            $event_type = 'tag_push';
 | 
						|
            $is_release = true;
 | 
						|
        }
 | 
						|
        // GitHub release webhook
 | 
						|
        elseif (isset($data['action']) && isset($data['release']) && $data['action'] === 'published') {
 | 
						|
            $event_type = 'github_release';
 | 
						|
            $is_release = true;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Log webhook event
 | 
						|
        $wpdb->insert(
 | 
						|
            $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
            array(
 | 
						|
                'product_id' => $product_id,
 | 
						|
                'event_type' => $event_type,
 | 
						|
                'payload' => $payload,
 | 
						|
                'processed' => 'pending',
 | 
						|
                'received_at' => current_time('mysql')
 | 
						|
            ),
 | 
						|
            array('%d', '%s', '%s', '%s', '%s')
 | 
						|
        );
 | 
						|
        
 | 
						|
        $event_id = $wpdb->insert_id;
 | 
						|
        
 | 
						|
        if (!$is_release) {
 | 
						|
            // Mark as ignored - not a release event
 | 
						|
            $wpdb->update(
 | 
						|
                $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
                array(
 | 
						|
                    'processed' => 'ignored',
 | 
						|
                    'processed_at' => current_time('mysql'),
 | 
						|
                    'error_message' => 'Not a release event'
 | 
						|
                ),
 | 
						|
                array('id' => $event_id),
 | 
						|
                array('%s', '%s', '%s'),
 | 
						|
                array('%d')
 | 
						|
            );
 | 
						|
            
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => true,
 | 
						|
                'message' => __('Webhook received but not a release event.', 'wp-digital-download')
 | 
						|
            ), 200);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Extract version information based on platform
 | 
						|
        $version = '';
 | 
						|
        $tag = '';
 | 
						|
        
 | 
						|
        if ($event_type === 'release' || $event_type === 'github_release') {
 | 
						|
            // Gitea or GitHub release
 | 
						|
            $tag = $data['release']['tag_name'] ?? '';
 | 
						|
            $version = ltrim($tag, 'v');
 | 
						|
        } elseif ($event_type === 'tag_push') {
 | 
						|
            // Git tag push
 | 
						|
            $tag = str_replace('refs/tags/', '', $data['ref']);
 | 
						|
            $version = ltrim($tag, 'v');
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (empty($version)) {
 | 
						|
            // Mark as error
 | 
						|
            $wpdb->update(
 | 
						|
                $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
                array(
 | 
						|
                    'processed' => 'error',
 | 
						|
                    'processed_at' => current_time('mysql'),
 | 
						|
                    'error_message' => 'Could not extract version from payload'
 | 
						|
                ),
 | 
						|
                array('id' => $event_id),
 | 
						|
                array('%s', '%s', '%s'),
 | 
						|
                array('%d')
 | 
						|
            );
 | 
						|
            
 | 
						|
            return new WP_REST_Response(array(
 | 
						|
                'success' => false,
 | 
						|
                'message' => __('Could not extract version from webhook payload.', 'wp-digital-download')
 | 
						|
            ), 400);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if this version already exists
 | 
						|
        $existing = $wpdb->get_var($wpdb->prepare(
 | 
						|
            "SELECT id FROM {$wpdb->prefix}wpdd_software_versions 
 | 
						|
             WHERE product_id = %d AND version = %s",
 | 
						|
            $product_id,
 | 
						|
            $version
 | 
						|
        ));
 | 
						|
        
 | 
						|
        if (!$existing) {
 | 
						|
            // Process new release
 | 
						|
            $success = self::process_new_release($product_id, $version, $tag, $data);
 | 
						|
            
 | 
						|
            if ($success) {
 | 
						|
                // Mark webhook as processed
 | 
						|
                $wpdb->update(
 | 
						|
                    $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
                    array(
 | 
						|
                        'processed' => 'completed',
 | 
						|
                        'processed_at' => current_time('mysql')
 | 
						|
                    ),
 | 
						|
                    array('id' => $event_id),
 | 
						|
                    array('%s', '%s'),
 | 
						|
                    array('%d')
 | 
						|
                );
 | 
						|
            } else {
 | 
						|
                // Mark as error
 | 
						|
                $wpdb->update(
 | 
						|
                    $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
                    array(
 | 
						|
                        'processed' => 'error',
 | 
						|
                        'processed_at' => current_time('mysql'),
 | 
						|
                        'error_message' => 'Failed to process release'
 | 
						|
                    ),
 | 
						|
                    array('id' => $event_id),
 | 
						|
                    array('%s', '%s', '%s'),
 | 
						|
                    array('%d')
 | 
						|
                );
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            // Mark as duplicate
 | 
						|
            $wpdb->update(
 | 
						|
                $wpdb->prefix . 'wpdd_webhook_events',
 | 
						|
                array(
 | 
						|
                    'processed' => 'duplicate',
 | 
						|
                    'processed_at' => current_time('mysql'),
 | 
						|
                    'error_message' => 'Version already exists'
 | 
						|
                ),
 | 
						|
                array('id' => $event_id),
 | 
						|
                array('%s', '%s', '%s'),
 | 
						|
                array('%d')
 | 
						|
            );
 | 
						|
        }
 | 
						|
        
 | 
						|
        return new WP_REST_Response(array(
 | 
						|
            'success' => true,
 | 
						|
            'message' => __('Webhook received and processed.', 'wp-digital-download')
 | 
						|
        ), 200);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Process new release from webhook (receives data FROM Git platforms like Gitea)
 | 
						|
     */
 | 
						|
    private static function process_new_release($product_id, $version, $tag, $webhook_data) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Get Git repository settings
 | 
						|
        $git_url = get_post_meta($product_id, '_wpdd_git_repository', true);
 | 
						|
        $git_username = get_post_meta($product_id, '_wpdd_git_username', true);
 | 
						|
        $git_token = get_post_meta($product_id, '_wpdd_git_token', true);
 | 
						|
        
 | 
						|
        if (!$git_url) {
 | 
						|
            error_log('WPDD: No Git URL configured for product ' . $product_id);
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Build package from Git repository at the specific tag
 | 
						|
        $package_url = self::build_package_from_git($product_id, $git_url, $tag, $git_username, $git_token);
 | 
						|
        
 | 
						|
        if (!$package_url) {
 | 
						|
            error_log('WPDD: Failed to build package for product ' . $product_id . ' version ' . $version);
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Extract changelog based on webhook source
 | 
						|
        $changelog = '';
 | 
						|
        $git_commit = null;
 | 
						|
        
 | 
						|
        // Gitea/GitHub release with description
 | 
						|
        if (isset($webhook_data['release'])) {
 | 
						|
            $changelog = $webhook_data['release']['body'] ?? $webhook_data['release']['note'] ?? '';
 | 
						|
            $git_commit = $webhook_data['release']['target_commitish'] ?? null;
 | 
						|
        }
 | 
						|
        // Git push webhook - use commit messages
 | 
						|
        elseif (isset($webhook_data['commits']) && is_array($webhook_data['commits'])) {
 | 
						|
            $messages = array();
 | 
						|
            foreach ($webhook_data['commits'] as $commit) {
 | 
						|
                if (isset($commit['message'])) {
 | 
						|
                    $messages[] = '- ' . $commit['message'];
 | 
						|
                }
 | 
						|
            }
 | 
						|
            $changelog = implode("\n", $messages);
 | 
						|
            $git_commit = $webhook_data['after'] ?? $webhook_data['head_commit']['id'] ?? null;
 | 
						|
        }
 | 
						|
        // Fallback - try to get from head commit
 | 
						|
        elseif (isset($webhook_data['head_commit']['message'])) {
 | 
						|
            $changelog = '- ' . $webhook_data['head_commit']['message'];
 | 
						|
            $git_commit = $webhook_data['head_commit']['id'] ?? null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Insert new version
 | 
						|
        $result = $wpdb->insert(
 | 
						|
            $wpdb->prefix . 'wpdd_software_versions',
 | 
						|
            array(
 | 
						|
                'product_id' => $product_id,
 | 
						|
                'version' => $version,
 | 
						|
                'changelog' => $changelog,
 | 
						|
                'package_url' => $package_url,
 | 
						|
                'git_tag' => $tag,
 | 
						|
                'git_commit' => $git_commit,
 | 
						|
                'release_date' => current_time('mysql')
 | 
						|
            ),
 | 
						|
            array('%d', '%s', '%s', '%s', '%s', '%s', '%s')
 | 
						|
        );
 | 
						|
        
 | 
						|
        if ($result === false) {
 | 
						|
            error_log('WPDD: Failed to insert version record for product ' . $product_id . ' version ' . $version);
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Update product version meta
 | 
						|
        update_post_meta($product_id, '_wpdd_current_version', $version);
 | 
						|
        
 | 
						|
        // Update product files to include the new package
 | 
						|
        $files = get_post_meta($product_id, '_wpdd_files', true);
 | 
						|
        if (!is_array($files)) {
 | 
						|
            $files = array();
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Add or update the package file in the files list
 | 
						|
        $package_file = array(
 | 
						|
            'id' => 'package_' . $version,
 | 
						|
            'name' => get_the_title($product_id) . ' v' . $version,
 | 
						|
            'url' => $package_url
 | 
						|
        );
 | 
						|
        
 | 
						|
        // Remove any existing package entries and add the new one as the first file
 | 
						|
        $files = array_filter($files, function($file) {
 | 
						|
            return !isset($file['id']) || strpos($file['id'], 'package_') !== 0;
 | 
						|
        });
 | 
						|
        array_unshift($files, $package_file);
 | 
						|
        
 | 
						|
        update_post_meta($product_id, '_wpdd_files', $files);
 | 
						|
        
 | 
						|
        // Notify customers about update (optional)
 | 
						|
        self::notify_customers_about_update($product_id, $version);
 | 
						|
        
 | 
						|
        error_log('WPDD: Successfully processed new release for product ' . $product_id . ' version ' . $version);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Build package from Git repository at specific tag
 | 
						|
     */
 | 
						|
    private static function build_package_from_git($product_id, $git_url, $tag, $username = null, $token = null) {
 | 
						|
        $upload_dir = wp_upload_dir();
 | 
						|
        $package_dir = trailingslashit($upload_dir['basedir']) . 'wpdd-packages/' . $product_id;
 | 
						|
        
 | 
						|
        if (!file_exists($package_dir)) {
 | 
						|
            wp_mkdir_p($package_dir);
 | 
						|
        }
 | 
						|
        
 | 
						|
        $package_filename = sanitize_file_name("package-{$tag}.zip");
 | 
						|
        $package_path = trailingslashit($package_dir) . $package_filename;
 | 
						|
        $package_url = trailingslashit($upload_dir['baseurl']) . 'wpdd-packages/' . $product_id . '/' . $package_filename;
 | 
						|
        
 | 
						|
        // Skip if package already exists
 | 
						|
        if (file_exists($package_path)) {
 | 
						|
            return $package_url;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Create temporary directory for cloning
 | 
						|
        $temp_dir = trailingslashit(sys_get_temp_dir()) . 'wpdd-build-' . $product_id . '-' . uniqid();
 | 
						|
        
 | 
						|
        // Build authentication URL if credentials provided
 | 
						|
        $auth_url = $git_url;
 | 
						|
        if ($username && $token) {
 | 
						|
            $parsed_url = parse_url($git_url);
 | 
						|
            if ($parsed_url) {
 | 
						|
                $auth_url = $parsed_url['scheme'] . '://' . urlencode($username) . ':' . urlencode($token) . '@' . $parsed_url['host'];
 | 
						|
                if (isset($parsed_url['port'])) {
 | 
						|
                    $auth_url .= ':' . $parsed_url['port'];
 | 
						|
                }
 | 
						|
                $auth_url .= $parsed_url['path'];
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Clone repository at specific tag
 | 
						|
        $clone_cmd = sprintf(
 | 
						|
            'git clone --depth 1 --branch %s %s %s 2>&1',
 | 
						|
            escapeshellarg($tag),
 | 
						|
            escapeshellarg($auth_url),
 | 
						|
            escapeshellarg($temp_dir)
 | 
						|
        );
 | 
						|
        
 | 
						|
        $output = array();
 | 
						|
        $return_code = 0;
 | 
						|
        exec($clone_cmd, $output, $return_code);
 | 
						|
        
 | 
						|
        if ($return_code !== 0) {
 | 
						|
            error_log('WPDD: Git clone failed for ' . $git_url . ' tag ' . $tag . ': ' . implode(' ', $output));
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Remove .git directory and other development files
 | 
						|
        $cleanup_files = array('.git', '.gitignore', '.gitattributes', 'tests', 'test', '.phpunit.xml', 'composer.json', 'package.json');
 | 
						|
        foreach ($cleanup_files as $cleanup_file) {
 | 
						|
            $cleanup_path = trailingslashit($temp_dir) . $cleanup_file;
 | 
						|
            if (file_exists($cleanup_path)) {
 | 
						|
                if (is_dir($cleanup_path)) {
 | 
						|
                    self::remove_directory($cleanup_path);
 | 
						|
                } else {
 | 
						|
                    unlink($cleanup_path);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Create ZIP package
 | 
						|
        if (class_exists('ZipArchive')) {
 | 
						|
            $zip = new ZipArchive();
 | 
						|
            if ($zip->open($package_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
 | 
						|
                self::add_directory_to_zip($zip, $temp_dir, '');
 | 
						|
                $zip->close();
 | 
						|
                
 | 
						|
                // Clean up temporary directory
 | 
						|
                self::remove_directory($temp_dir);
 | 
						|
                
 | 
						|
                return $package_url;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Fallback: tar command if ZipArchive not available
 | 
						|
        $tar_cmd = sprintf(
 | 
						|
            'cd %s && tar -czf %s . 2>&1',
 | 
						|
            escapeshellarg($temp_dir),
 | 
						|
            escapeshellarg($package_path . '.tar.gz')
 | 
						|
        );
 | 
						|
        
 | 
						|
        exec($tar_cmd, $output, $return_code);
 | 
						|
        self::remove_directory($temp_dir);
 | 
						|
        
 | 
						|
        if ($return_code === 0 && file_exists($package_path . '.tar.gz')) {
 | 
						|
            return $package_url . '.tar.gz';
 | 
						|
        }
 | 
						|
        
 | 
						|
        error_log('WPDD: Failed to create package for ' . $git_url . ' tag ' . $tag);
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Recursively add directory to ZIP archive
 | 
						|
     */
 | 
						|
    private static function add_directory_to_zip($zip, $dir, $base_path) {
 | 
						|
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
 | 
						|
        foreach ($iterator as $file) {
 | 
						|
            if ($file->isFile()) {
 | 
						|
                $file_path = $file->getPathname();
 | 
						|
                $relative_path = $base_path . substr($file_path, strlen($dir) + 1);
 | 
						|
                $zip->addFile($file_path, $relative_path);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Recursively remove directory
 | 
						|
     */
 | 
						|
    private static function remove_directory($dir) {
 | 
						|
        if (!is_dir($dir)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $iterator = new RecursiveIteratorIterator(
 | 
						|
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
 | 
						|
            RecursiveIteratorIterator::CHILD_FIRST
 | 
						|
        );
 | 
						|
        
 | 
						|
        foreach ($iterator as $file) {
 | 
						|
            if ($file->isDir()) {
 | 
						|
                rmdir($file->getPathname());
 | 
						|
            } else {
 | 
						|
                unlink($file->getPathname());
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        rmdir($dir);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Notify customers about new update
 | 
						|
     */
 | 
						|
    private static function notify_customers_about_update($product_id, $version) {
 | 
						|
        // Optional: Send email notifications to customers with active licenses
 | 
						|
        // This could be a separate scheduled job to avoid timeout issues
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Manually sync a software product with its latest Git release
 | 
						|
     * This can be used to fix products that don't have files or need updates
 | 
						|
     */
 | 
						|
    public static function sync_software_product($product_id, $force_rebuild = false) {
 | 
						|
        global $wpdb;
 | 
						|
        
 | 
						|
        // Check if product is software license type
 | 
						|
        $product_type = get_post_meta($product_id, '_wpdd_product_type', true);
 | 
						|
        if ($product_type !== 'software_license') {
 | 
						|
            return array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => 'not_software_license',
 | 
						|
                'message' => __('Product is not a software license product.', 'wp-digital-download')
 | 
						|
            );
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Get Git repository settings
 | 
						|
        $git_url = get_post_meta($product_id, '_wpdd_git_repository', true);
 | 
						|
        $git_username = get_post_meta($product_id, '_wpdd_git_username', true);
 | 
						|
        $git_token = get_post_meta($product_id, '_wpdd_git_token', true);
 | 
						|
        $current_version = get_post_meta($product_id, '_wpdd_current_version', true);
 | 
						|
        
 | 
						|
        if (!$git_url) {
 | 
						|
            return array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => 'no_git_url',
 | 
						|
                'message' => __('No Git repository URL configured.', 'wp-digital-download')
 | 
						|
            );
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (!$current_version) {
 | 
						|
            return array(
 | 
						|
                'success' => false,
 | 
						|
                'error' => 'no_version',
 | 
						|
                'message' => __('No current version specified. Please set a version in the product settings.', 'wp-digital-download')
 | 
						|
            );
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Check if we already have this version
 | 
						|
        $existing_version = $wpdb->get_row($wpdb->prepare(
 | 
						|
            "SELECT * FROM {$wpdb->prefix}wpdd_software_versions 
 | 
						|
             WHERE product_id = %d AND version = %s",
 | 
						|
            $product_id,
 | 
						|
            $current_version
 | 
						|
        ));
 | 
						|
        
 | 
						|
        $package_url = null;
 | 
						|
        
 | 
						|
        if (!$existing_version || $force_rebuild) {
 | 
						|
            // Build package from current version
 | 
						|
            $package_url = self::build_package_from_git($product_id, $git_url, 'v' . $current_version, $git_username, $git_token);
 | 
						|
            
 | 
						|
            if (!$package_url) {
 | 
						|
                // Try without 'v' prefix
 | 
						|
                $package_url = self::build_package_from_git($product_id, $git_url, $current_version, $git_username, $git_token);
 | 
						|
            }
 | 
						|
            
 | 
						|
            if (!$package_url) {
 | 
						|
                return array(
 | 
						|
                    'success' => false,
 | 
						|
                    'error' => 'build_failed',
 | 
						|
                    'message' => __('Failed to build package from repository.', 'wp-digital-download')
 | 
						|
                );
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Insert or update version record
 | 
						|
            if ($existing_version) {
 | 
						|
                $wpdb->update(
 | 
						|
                    $wpdb->prefix . 'wpdd_software_versions',
 | 
						|
                    array(
 | 
						|
                        'package_url' => $package_url,
 | 
						|
                        'release_date' => current_time('mysql')
 | 
						|
                    ),
 | 
						|
                    array('id' => $existing_version->id),
 | 
						|
                    array('%s', '%s'),
 | 
						|
                    array('%d')
 | 
						|
                );
 | 
						|
            } else {
 | 
						|
                $wpdb->insert(
 | 
						|
                    $wpdb->prefix . 'wpdd_software_versions',
 | 
						|
                    array(
 | 
						|
                        'product_id' => $product_id,
 | 
						|
                        'version' => $current_version,
 | 
						|
                        'package_url' => $package_url,
 | 
						|
                        'git_tag' => 'v' . $current_version,
 | 
						|
                        'release_date' => current_time('mysql')
 | 
						|
                    ),
 | 
						|
                    array('%d', '%s', '%s', '%s', '%s')
 | 
						|
                );
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            $package_url = $existing_version->package_url;
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Update product files to include the package
 | 
						|
        $files = get_post_meta($product_id, '_wpdd_files', true);
 | 
						|
        if (!is_array($files)) {
 | 
						|
            $files = array();
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Add or update the package file in the files list
 | 
						|
        $package_file = array(
 | 
						|
            'id' => 'package_' . $current_version,
 | 
						|
            'name' => get_the_title($product_id) . ' v' . $current_version,
 | 
						|
            'url' => $package_url
 | 
						|
        );
 | 
						|
        
 | 
						|
        // Remove any existing package entries and add the new one as the first file
 | 
						|
        $files = array_filter($files, function($file) {
 | 
						|
            return !isset($file['id']) || strpos($file['id'], 'package_') !== 0;
 | 
						|
        });
 | 
						|
        array_unshift($files, $package_file);
 | 
						|
        
 | 
						|
        update_post_meta($product_id, '_wpdd_files', $files);
 | 
						|
        
 | 
						|
        return array(
 | 
						|
            'success' => true,
 | 
						|
            'message' => __('Product synced successfully.', 'wp-digital-download'),
 | 
						|
            'version' => $current_version,
 | 
						|
            'package_url' => $package_url
 | 
						|
        );
 | 
						|
    }
 | 
						|
} |