'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[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[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\d+)/(?P[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 ); } }