Files
wp-digital-download/includes/class-wpdd-paypal.php
jknapp 4731637f33
Some checks failed
Create Release / build (push) Failing after 3s
Major improvements: Fix download limits, enhance license display, fix software filenames
🔧 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>
2025-09-09 19:16:57 -07:00

367 lines
14 KiB
PHP

<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_PayPal {
public static function init() {
add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_paypal_sdk'));
add_action('wp_ajax_wpdd_create_paypal_order', array(__CLASS__, 'create_order'));
add_action('wp_ajax_nopriv_wpdd_create_paypal_order', array(__CLASS__, 'create_order'));
add_action('wp_ajax_wpdd_capture_paypal_order', array(__CLASS__, 'capture_order'));
add_action('wp_ajax_nopriv_wpdd_capture_paypal_order', array(__CLASS__, 'capture_order'));
}
public static function enqueue_paypal_sdk() {
// Check if we're on a page with the checkout shortcode or if there's a product_id in the URL (checkout page)
global $post;
$is_checkout = false;
// Check if we're on the configured checkout page
if (is_page(get_option('wpdd_checkout_page_id'))) {
$is_checkout = true;
}
// Also check if the current page has the checkout shortcode
if ($post && has_shortcode($post->post_content, 'wpdd_checkout')) {
$is_checkout = true;
}
// Also check if there's a product_id parameter (checkout flow)
if (isset($_GET['product_id']) || isset($_GET['wpdd_action'])) {
$is_checkout = true;
}
if (!$is_checkout) {
return;
}
$client_id = get_option('wpdd_paypal_client_id');
$mode = get_option('wpdd_paypal_mode', 'sandbox');
if (!$client_id) {
return;
}
wp_enqueue_script(
'paypal-sdk',
'https://www.paypal.com/sdk/js?client-id=' . $client_id . '&currency=USD',
array(),
null,
true
);
wp_enqueue_script(
'wpdd-paypal',
WPDD_PLUGIN_URL . 'assets/js/paypal.js',
array('jquery', 'paypal-sdk'),
WPDD_VERSION,
true
);
wp_localize_script('wpdd-paypal', 'wpdd_paypal', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wpdd-paypal-nonce'),
'mode' => $mode
));
}
public static function create_order() {
check_ajax_referer('wpdd-paypal-nonce', 'nonce');
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error('Invalid product');
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
wp_send_json_error('Product not found');
}
$price = get_post_meta($product_id, '_wpdd_price', true);
$sale_price = get_post_meta($product_id, '_wpdd_sale_price', true);
$final_price = ($sale_price && $sale_price < $price) ? $sale_price : $price;
$order_data = array(
'intent' => 'CAPTURE',
'purchase_units' => array(
array(
'reference_id' => 'wpdd_' . $product_id . '_' . time(),
'description' => substr($product->post_title, 0, 127),
'amount' => array(
'currency_code' => 'USD',
'value' => number_format($final_price, 2, '.', '')
)
)
),
'application_context' => array(
'brand_name' => get_bloginfo('name'),
'return_url' => add_query_arg('wpdd_paypal_return', '1', get_permalink(get_option('wpdd_thank_you_page_id'))),
'cancel_url' => add_query_arg('wpdd_paypal_cancel', '1', get_permalink(get_option('wpdd_checkout_page_id')))
)
);
$paypal_order = self::api_request('/v2/checkout/orders', $order_data, 'POST');
if (isset($paypal_order['id'])) {
// Ensure session is available before storing data
if (WP_Digital_Download::ensure_session()) {
$_SESSION['wpdd_paypal_order_' . $paypal_order['id']] = array(
'product_id' => $product_id,
'amount' => $final_price,
'customer_email' => sanitize_email($_POST['customer_email'] ?? ''),
'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '')
);
} else {
// Fallback: use WordPress options table with short expiry
set_transient('wpdd_paypal_order_' . $paypal_order['id'], array(
'product_id' => $product_id,
'amount' => $final_price,
'customer_email' => sanitize_email($_POST['customer_email'] ?? ''),
'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '')
), 600); // 10 minutes
}
wp_send_json_success(array('orderID' => $paypal_order['id']));
} else {
wp_send_json_error('Failed to create PayPal order');
}
}
public static function capture_order() {
check_ajax_referer('wpdd-paypal-nonce', 'nonce');
$paypal_order_id = isset($_POST['orderID']) ? sanitize_text_field($_POST['orderID']) : '';
if (!$paypal_order_id) {
wp_send_json_error('Invalid order ID');
}
$capture_response = self::api_request('/v2/checkout/orders/' . $paypal_order_id . '/capture', array(), 'POST');
if (isset($capture_response['status']) && $capture_response['status'] === 'COMPLETED') {
// Try to get data from session first, then fallback to transient
$session_data = array();
if (WP_Digital_Download::ensure_session() && isset($_SESSION['wpdd_paypal_order_' . $paypal_order_id])) {
$session_data = $_SESSION['wpdd_paypal_order_' . $paypal_order_id];
} else {
// Try transient fallback
$session_data = get_transient('wpdd_paypal_order_' . $paypal_order_id);
if ($session_data === false) {
$session_data = array();
}
}
// Add error logging for debugging session issues
if (empty($session_data)) {
error_log('WPDD PayPal Error: No session data found for PayPal order ' . $paypal_order_id);
error_log('WPDD PayPal Debug: Session ID: ' . session_id());
error_log('WPDD PayPal Debug: Available sessions: ' . print_r($_SESSION ?? array(), true));
wp_send_json_error('Session data not found - order cannot be processed');
return;
}
$order_number = 'WPDD-' . strtoupper(uniqid());
global $wpdb;
$customer_id = 0;
$customer_email = $session_data['customer_email'];
$customer_name = $session_data['customer_name'];
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
$customer_id = $current_user->ID;
$customer_email = $current_user->user_email;
$customer_name = $current_user->display_name;
} elseif (!empty($_POST['create_account']) && !empty($customer_email)) {
$username = strstr($customer_email, '@', true) . '_' . wp_rand(1000, 9999);
$password = wp_generate_password();
$user_id = wp_create_user($username, $password, $customer_email);
if (!is_wp_error($user_id)) {
$customer_id = $user_id;
wp_update_user(array(
'ID' => $user_id,
'display_name' => $customer_name,
'first_name' => $customer_name
));
$user = new WP_User($user_id);
$user->set_role('wpdd_customer');
wp_new_user_notification($user_id, null, 'both');
}
}
$product = get_post($session_data['product_id']);
$wpdb->insert(
$wpdb->prefix . 'wpdd_orders',
array(
'order_number' => $order_number,
'product_id' => $session_data['product_id'],
'customer_id' => $customer_id,
'creator_id' => $product->post_author,
'status' => 'completed',
'payment_method' => 'paypal',
'transaction_id' => $capture_response['id'],
'amount' => $session_data['amount'],
'currency' => 'USD',
'customer_email' => $customer_email,
'customer_name' => $customer_name,
'purchase_date' => current_time('mysql')
),
array('%s', '%d', '%d', '%d', '%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s')
);
$order_id = $wpdb->insert_id;
// Trigger the order completed hook for balance tracking
do_action('wpdd_order_completed', $order_id);
self::generate_download_link($order_id);
self::send_purchase_email($order_id);
update_post_meta($session_data['product_id'], '_wpdd_sales_count',
intval(get_post_meta($session_data['product_id'], '_wpdd_sales_count', true)) + 1);
// Clean up stored data (both session and transient)
if (WP_Digital_Download::ensure_session() && isset($_SESSION['wpdd_paypal_order_' . $paypal_order_id])) {
unset($_SESSION['wpdd_paypal_order_' . $paypal_order_id]);
}
delete_transient('wpdd_paypal_order_' . $paypal_order_id);
wp_send_json_success(array(
'redirect_url' => add_query_arg(
'order_id',
$order_number,
get_permalink(get_option('wpdd_thank_you_page_id'))
)
));
} else {
wp_send_json_error('Payment capture failed');
}
}
private static function api_request($endpoint, $data = array(), $method = 'GET') {
$mode = get_option('wpdd_paypal_mode', 'sandbox');
$client_id = get_option('wpdd_paypal_client_id');
$secret = get_option('wpdd_paypal_secret');
if (!$client_id || !$secret) {
return false;
}
$base_url = $mode === 'live'
? 'https://api.paypal.com'
: 'https://api.sandbox.paypal.com';
$auth = base64_encode($client_id . ':' . $secret);
$args = array(
'method' => $method,
'headers' => array(
'Authorization' => 'Basic ' . $auth,
'Content-Type' => 'application/json'
),
'timeout' => 30
);
if (!empty($data)) {
$args['body'] = json_encode($data);
}
$response = wp_remote_request($base_url . $endpoint, $args);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
private static function generate_download_link($order_id) {
global $wpdb;
// Get the order to find the product
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d",
$order_id
));
if (!$order) {
return false;
}
// Get download limit from product settings
$download_limit = get_post_meta($order->product_id, '_wpdd_download_limit', true);
$download_limit = $download_limit ?: 0; // Default to 0 (unlimited) if not set
// Get download expiry from product settings
$download_expiry = get_post_meta($order->product_id, '_wpdd_download_expiry', true);
$download_expiry = $download_expiry ?: 30; // Default to 30 days if not set
$token = wp_hash(uniqid() . $order_id . time());
$expires_at = date('Y-m-d H:i:s', strtotime('+' . $download_expiry . ' days'));
$wpdb->insert(
$wpdb->prefix . 'wpdd_download_links',
array(
'order_id' => $order_id,
'token' => $token,
'expires_at' => $expires_at,
'max_downloads' => $download_limit,
'created_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%d', '%s')
);
return $token;
}
private static function send_purchase_email($order_id) {
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT o.*, p.post_title as product_name, dl.token
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
LEFT JOIN {$wpdb->prefix}wpdd_download_links dl ON o.id = dl.order_id
WHERE o.id = %d",
$order_id
));
if (!$order) {
return;
}
$download_url = add_query_arg(array(
'wpdd_download_token' => $order->token
), home_url());
$subject = sprintf(__('Your Purchase from %s', 'wp-digital-download'), get_bloginfo('name'));
$message = sprintf(
__("Hi %s,\n\nThank you for your purchase!\n\n", 'wp-digital-download'),
$order->customer_name
);
$message .= sprintf(__("Order Number: %s\n", 'wp-digital-download'), $order->order_number);
$message .= sprintf(__("Product: %s\n", 'wp-digital-download'), $order->product_name);
$message .= sprintf(__("Amount: $%s\n\n", 'wp-digital-download'), number_format($order->amount, 2));
$message .= __("Download your product here:\n", 'wp-digital-download');
$message .= $download_url . "\n\n";
$message .= __("This download link will expire in 7 days.\n\n", 'wp-digital-download');
$message .= sprintf(__("Best regards,\n%s", 'wp-digital-download'), get_bloginfo('name'));
wp_mail($order->customer_email, $subject, $message);
}
}