diff --git a/admin/class-wpdd-admin-payouts.php b/admin/class-wpdd-admin-payouts.php index 3ba64db..35beedc 100644 --- a/admin/class-wpdd-admin-payouts.php +++ b/admin/class-wpdd-admin-payouts.php @@ -10,6 +10,10 @@ class WPDD_Admin_Payouts { add_action('admin_menu', array(__CLASS__, 'add_menu_page')); add_action('admin_post_wpdd_process_payout', array(__CLASS__, 'process_payout')); add_action('admin_post_wpdd_bulk_payouts', array(__CLASS__, 'process_bulk_payouts')); + add_action('admin_post_wpdd_process_payout_request', array(__CLASS__, 'process_payout_request')); + add_action('admin_post_wpdd_reject_payout_request', array(__CLASS__, 'reject_payout_request')); + add_action('admin_post_wpdd_manual_payout', array(__CLASS__, 'process_manual_payout')); + add_action('admin_post_wpdd_adjust_balance', array(__CLASS__, 'adjust_creator_balance')); add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_scripts')); } @@ -50,6 +54,15 @@ class WPDD_Admin_Payouts { $currency = get_option('wpdd_currency', 'USD'); $threshold = floatval(get_option('wpdd_payout_threshold', 0)); + // Get payout requests (requested status) + $payout_requests = $wpdb->get_results( + "SELECT p.*, u.display_name, u.user_email + FROM {$wpdb->prefix}wpdd_payouts p + INNER JOIN {$wpdb->users} u ON p.creator_id = u.ID + WHERE p.status = 'requested' + ORDER BY p.created_at ASC" + ); + // Get payout history $query = "SELECT p.*, u.display_name, u.user_email FROM {$wpdb->prefix}wpdd_payouts p @@ -81,9 +94,172 @@ class WPDD_Admin_Payouts {

+ +
+

+
+ +
+

()

+

+ + + + + + + + + + + + + + + + + + + + + + +
+ display_name); ?>
+ user_email); ?> +
amount, $request->currency); ?>paypal_email); ?>created_at))); ?> +
+ + + id, 'wpdd_nonce'); ?> + +
+
+ + + id, 'wpdd_nonce'); ?> + +
+
+
+ + + +
+ +
+

+

+ +
+ + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ +

+ +

+
+
+ + +
+

+

+ +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+ +
+ +

+ +

+
+
+
+

@@ -432,4 +608,241 @@ class WPDD_Admin_Payouts { return false; } } + + public static function process_payout_request() { + if (!current_user_can('manage_options')) { + wp_die(__('You do not have permission to perform this action.', 'wp-digital-download')); + } + + $payout_id = isset($_POST['payout_id']) ? intval($_POST['payout_id']) : 0; + + if (!$payout_id || !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_process_payout_request_' . $payout_id)) { + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + exit; + } + + global $wpdb; + + // Get the payout request + $payout = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d AND status = 'requested'", + $payout_id + )); + + if (!$payout) { + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + exit; + } + + // Process via PayPal API + $result = WPDD_PayPal_Payouts::process_payout($payout_id); + + if ($result['success']) { + // Update payout status + $wpdb->update( + $wpdb->prefix . 'wpdd_payouts', + array( + 'status' => 'completed', + 'transaction_id' => $result['transaction_id'], + 'processed_by' => get_current_user_id(), + 'processed_at' => current_time('mysql') + ), + array('id' => $payout_id), + array('%s', '%s', '%d', '%s'), + array('%d') + ); + + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success')); + } else { + // Update with error + $wpdb->update( + $wpdb->prefix . 'wpdd_payouts', + array( + 'status' => 'failed', + 'notes' => $result['error'] + ), + array('id' => $payout_id), + array('%s', '%s'), + array('%d') + ); + + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + } + exit; + } + + public static function reject_payout_request() { + if (!current_user_can('manage_options')) { + wp_die(__('You do not have permission to perform this action.', 'wp-digital-download')); + } + + $payout_id = isset($_POST['payout_id']) ? intval($_POST['payout_id']) : 0; + + if (!$payout_id || !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_reject_payout_request_' . $payout_id)) { + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + exit; + } + + global $wpdb; + + // Get the payout request + $payout = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d AND status = 'requested'", + $payout_id + )); + + if (!$payout) { + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error')); + exit; + } + + // Update status to failed/rejected + $wpdb->update( + $wpdb->prefix . 'wpdd_payouts', + array( + 'status' => 'failed', + 'notes' => 'Request rejected by administrator', + 'processed_by' => get_current_user_id(), + 'processed_at' => current_time('mysql') + ), + array('id' => $payout_id), + array('%s', '%s', '%d', '%s'), + array('%d') + ); + + // Restore balance to creator + update_user_meta($payout->creator_id, 'wpdd_creator_balance', $payout->amount); + + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success')); + exit; + } + + public static function process_manual_payout() { + if (!current_user_can('manage_options') || + !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_manual_payout')) { + wp_die(__('Security check failed', 'wp-digital-download')); + } + + $creator_id = intval($_POST['creator_id']); + $amount = floatval($_POST['payout_amount']); + $reason = sanitize_textarea_field($_POST['payout_reason']); + + if (!$creator_id || $amount <= 0) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + $creator = get_userdata($creator_id); + if (!$creator || !in_array('wpdd_creator', $creator->roles)) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + $paypal_email = get_user_meta($creator_id, 'wpdd_paypal_email', true); + if (empty($paypal_email)) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + global $wpdb; + + // Create the payout record + $result = $wpdb->insert( + $wpdb->prefix . 'wpdd_payouts', + array( + 'creator_id' => $creator_id, + 'amount' => $amount, + 'currency' => get_option('wpdd_currency', 'USD'), + 'paypal_email' => $paypal_email, + 'status' => 'pending', + 'payout_method' => 'manual', + 'notes' => $reason, + 'created_at' => current_time('mysql'), + 'processed_by' => get_current_user_id() + ), + array('%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%d') + ); + + if (!$result) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + $payout_id = $wpdb->insert_id; + + // Try to process via PayPal + if (class_exists('WPDD_PayPal_Payouts')) { + $paypal_result = WPDD_PayPal_Payouts::process_payout($payout_id); + + if (!$paypal_result) { + // Update status to failed + $wpdb->update( + $wpdb->prefix . 'wpdd_payouts', + array('status' => 'failed'), + array('id' => $payout_id), + array('%s'), + array('%d') + ); + } + } + + wp_redirect(add_query_arg('message', 'success', wp_get_referer())); + exit; + } + + public static function adjust_creator_balance() { + if (!current_user_can('manage_options') || + !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_adjust_balance')) { + wp_die(__('Security check failed', 'wp-digital-download')); + } + + $creator_id = intval($_POST['creator_id']); + $adjustment_type = sanitize_text_field($_POST['adjustment_type']); + $amount = floatval($_POST['adjustment_amount']); + $reason = sanitize_textarea_field($_POST['adjustment_reason']); + + if (!$creator_id || $amount <= 0 || !in_array($adjustment_type, array('add', 'subtract'))) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + $creator = get_userdata($creator_id); + if (!$creator || !in_array('wpdd_creator', $creator->roles)) { + wp_redirect(add_query_arg('message', 'error', wp_get_referer())); + exit; + } + + // Get current balance + $current_balance = floatval(get_user_meta($creator_id, 'wpdd_balance', true)); + + // Calculate new balance + if ($adjustment_type === 'add') { + $new_balance = $current_balance + $amount; + } else { + $new_balance = max(0, $current_balance - $amount); // Don't allow negative balance + } + + // Update the balance + update_user_meta($creator_id, 'wpdd_balance', $new_balance); + + // Create a record of this adjustment + global $wpdb; + $wpdb->insert( + $wpdb->prefix . 'wpdd_balance_adjustments', + array( + 'creator_id' => $creator_id, + 'adjustment_type' => $adjustment_type, + 'amount' => $amount, + 'previous_balance' => $current_balance, + 'new_balance' => $new_balance, + 'reason' => $reason, + 'adjusted_by' => get_current_user_id(), + 'created_at' => current_time('mysql') + ), + array('%d', '%s', '%f', '%f', '%f', '%s', '%d', '%s') + ); + + wp_redirect(add_query_arg('message', 'balance_adjusted', wp_get_referer())); + exit; + } } \ No newline at end of file diff --git a/admin/class-wpdd-admin.php b/admin/class-wpdd-admin.php index 7ef9d78..db27ba9 100644 --- a/admin/class-wpdd-admin.php +++ b/admin/class-wpdd-admin.php @@ -12,6 +12,7 @@ class WPDD_Admin { add_action('manage_wpdd_product_posts_custom_column', array(__CLASS__, 'render_product_columns'), 10, 2); add_filter('manage_edit-wpdd_product_sortable_columns', array(__CLASS__, 'make_columns_sortable')); add_action('pre_get_posts', array(__CLASS__, 'sort_products_by_column')); + add_action('pre_get_posts', array(__CLASS__, 'filter_creator_products')); add_action('admin_init', array(__CLASS__, 'handle_admin_actions')); // Initialize admin payouts @@ -21,41 +22,70 @@ class WPDD_Admin { } public static function add_admin_menus() { - add_submenu_page( - 'edit.php?post_type=wpdd_product', - __('Orders', 'wp-digital-download'), - __('Orders', 'wp-digital-download'), - 'wpdd_manage_orders', - 'wpdd-orders', - array(__CLASS__, 'render_orders_page') - ); + // Show different menus based on user role + $user = wp_get_current_user(); + $is_creator = in_array('wpdd_creator', (array) $user->roles); + $is_admin = current_user_can('manage_options'); - add_submenu_page( - 'edit.php?post_type=wpdd_product', - __('Reports', 'wp-digital-download'), - __('Reports', 'wp-digital-download'), - 'wpdd_view_reports', - 'wpdd-reports', - array(__CLASS__, 'render_reports_page') - ); + if ($is_admin) { + // Full admin menus + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('Orders', 'wp-digital-download'), + __('Orders', 'wp-digital-download'), + 'wpdd_manage_orders', + 'wpdd-orders', + array(__CLASS__, 'render_orders_page') + ); + + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('Reports', 'wp-digital-download'), + __('Reports', 'wp-digital-download'), + 'wpdd_view_reports', + 'wpdd-reports', + array(__CLASS__, 'render_reports_page') + ); + + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('Customers', 'wp-digital-download'), + __('Customers', 'wp-digital-download'), + 'wpdd_manage_orders', + 'wpdd-customers', + array(__CLASS__, 'render_customers_page') + ); + + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('Shortcodes', 'wp-digital-download'), + __('Shortcodes', 'wp-digital-download'), + 'edit_wpdd_products', + 'wpdd-shortcodes', + array(__CLASS__, 'render_shortcodes_page') + ); + } - add_submenu_page( - 'edit.php?post_type=wpdd_product', - __('Customers', 'wp-digital-download'), - __('Customers', 'wp-digital-download'), - 'wpdd_manage_orders', - 'wpdd-customers', - array(__CLASS__, 'render_customers_page') - ); - - add_submenu_page( - 'edit.php?post_type=wpdd_product', - __('Shortcodes', 'wp-digital-download'), - __('Shortcodes', 'wp-digital-download'), - 'edit_wpdd_products', - 'wpdd-shortcodes', - array(__CLASS__, 'render_shortcodes_page') - ); + if ($is_creator || $is_admin) { + // Creator-specific menus + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('My Sales', 'wp-digital-download'), + __('My Sales', 'wp-digital-download'), + 'wpdd_view_own_sales', + 'wpdd-creator-sales', + array(__CLASS__, 'render_creator_sales_page') + ); + + add_submenu_page( + 'edit.php?post_type=wpdd_product', + __('My Payouts', 'wp-digital-download'), + __('My Payouts', 'wp-digital-download'), + 'wpdd_view_own_sales', + 'wpdd-creator-payouts', + array(__CLASS__, 'render_creator_payouts_page') + ); + } } public static function add_product_columns($columns) { @@ -917,4 +947,289 @@ class WPDD_Admin { is_main_query()) { + return; + } + + if (!isset($_GET['post_type']) || $_GET['post_type'] !== 'wpdd_product') { + return; + } + + $user = wp_get_current_user(); + $is_creator = in_array('wpdd_creator', (array) $user->roles); + $is_admin = current_user_can('manage_options'); + + // Only filter for creators, not admins + if ($is_creator && !$is_admin) { + $query->set('author', get_current_user_id()); + } + } + + public static function render_creator_sales_page() { + global $wpdb; + + $user_id = get_current_user_id(); + $currency = get_option('wpdd_currency', 'USD'); + $commission_rate = floatval(get_option('wpdd_commission_rate', 0)); + + // Get creator's sales data + $sales = $wpdb->get_results($wpdb->prepare( + "SELECT o.*, p.post_title as product_name, + (o.total * %f / 100) as platform_fee, + (o.total * (100 - %f) / 100) as creator_earning + FROM {$wpdb->prefix}wpdd_orders o + INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID + WHERE p.post_author = %d + AND o.status = 'completed' + ORDER BY o.purchase_date DESC + LIMIT 100", + $commission_rate, + $commission_rate, + $user_id + )); + + // Get totals + $total_sales = $wpdb->get_var($wpdb->prepare( + "SELECT SUM(o.total) + FROM {$wpdb->prefix}wpdd_orders o + INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID + WHERE p.post_author = %d + AND o.status = 'completed'", + $user_id + )); + + $total_earnings = $total_sales * (1 - ($commission_rate / 100)); + $current_balance = WPDD_Creator::get_creator_balance($user_id); + + ?> +
+

+ +
+
+

+
+
+
+

+
+ +
+
+

+
+ +
+
+ + +
+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
purchase_date))); ?>product_name); ?>customer_name); ?>total, $currency); ?>platform_fee, $currency); ?>creator_earning, $currency); ?> + + status)); ?> + +
+
+ +
+

+

+
+ +
+ get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_payouts + WHERE creator_id = %d + ORDER BY created_at DESC + LIMIT 50", + $user_id + )); + + ?> +
+

+ + +
+

+
+ + +
+

+ +
+
+

+
+ +
+ 0) : ?> +

+ +

+ +
+ +
+ +
+

+

+
+ +
+

+
+ +
+ +

+

+ +
+ +
+
+
+ + +
+

+ + + + + + + + + + + + + + + + + + + + + + + +
created_at))); ?>amount, $payout->currency); ?>paypal_email); ?> + '#fef3c7; color: #92400e;', + 'completed' => '#d1fae5; color: #065f46;', + 'failed' => '#fee2e2; color: #991b1b;', + 'requested' => '#dbeafe; color: #1e40af;' + ); + $status_color = isset($status_colors[$payout->status]) ? $status_colors[$payout->status] : '#f3f4f6; color: #374151;'; + ?> + + status)); ?> + + transaction_id ?: '-'); ?> + processed_at + ? esc_html(date_i18n(get_option('date_format'), strtotime($payout->processed_at))) + : '-'; + ?> +
+
+ +
+

+

+
+ +
+ insert( + $wpdb->prefix . 'wpdd_payouts', + array( + 'creator_id' => $user_id, + 'amount' => $balance, + 'currency' => $currency, + 'paypal_email' => $paypal_email, + 'status' => 'requested', + 'payout_method' => 'request', + 'created_at' => current_time('mysql') + ), + array('%d', '%f', '%s', '%s', '%s', '%s', '%s') + ); + + // Reset balance to 0 since it's now requested + update_user_meta($user_id, 'wpdd_creator_balance', 0); + + // Redirect to avoid resubmission + wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-creator-payouts&message=payout_requested')); + exit; + } } \ No newline at end of file diff --git a/admin/class-wpdd-settings.php b/admin/class-wpdd-settings.php index 07a729e..6240a4d 100644 --- a/admin/class-wpdd-settings.php +++ b/admin/class-wpdd-settings.php @@ -16,7 +16,7 @@ class WPDD_Settings { 'edit.php?post_type=wpdd_product', __('Settings', 'wp-digital-download'), __('Settings', 'wp-digital-download'), - 'wpdd_manage_settings', + 'manage_options', 'wpdd-settings', array(__CLASS__, 'render_settings_page') ); @@ -26,9 +26,16 @@ class WPDD_Settings { register_setting('wpdd_settings', 'wpdd_paypal_mode'); register_setting('wpdd_settings', 'wpdd_paypal_client_id'); register_setting('wpdd_settings', 'wpdd_paypal_secret'); + register_setting('wpdd_settings', 'wpdd_paypal_payout_email'); register_setting('wpdd_settings', 'wpdd_admin_email'); register_setting('wpdd_settings', 'wpdd_from_name'); register_setting('wpdd_settings', 'wpdd_from_email'); + register_setting('wpdd_settings', 'wpdd_smtp_enabled'); + register_setting('wpdd_settings', 'wpdd_smtp_host'); + register_setting('wpdd_settings', 'wpdd_smtp_port'); + register_setting('wpdd_settings', 'wpdd_smtp_username'); + register_setting('wpdd_settings', 'wpdd_smtp_password'); + register_setting('wpdd_settings', 'wpdd_smtp_encryption'); register_setting('wpdd_settings', 'wpdd_currency'); register_setting('wpdd_settings', 'wpdd_enable_guest_checkout'); register_setting('wpdd_settings', 'wpdd_default_download_limit'); @@ -193,6 +200,18 @@ class WPDD_Settings { 'wpdd_paypal_settings', array('name' => 'wpdd_paypal_secret') ); + + add_settings_field( + 'wpdd_paypal_payout_email', + __('PayPal Payout Account Email', 'wp-digital-download'), + array(__CLASS__, 'email_field'), + 'wpdd_settings', + 'wpdd_paypal_settings', + array( + 'name' => 'wpdd_paypal_payout_email', + 'description' => __('PayPal account email that will send payouts to creators', 'wp-digital-download') + ) + ); } private static function add_email_fields() { @@ -231,6 +250,94 @@ class WPDD_Settings { 'description' => __('Email address shown in email headers', 'wp-digital-download') ) ); + + add_settings_field( + 'wpdd_smtp_enabled', + __('Enable SMTP', 'wp-digital-download'), + array(__CLASS__, 'checkbox_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_enabled', + 'label' => __('Use SMTP for sending emails instead of PHP mail()', 'wp-digital-download') + ) + ); + + add_settings_field( + 'wpdd_smtp_host', + __('SMTP Host', 'wp-digital-download'), + array(__CLASS__, 'text_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_host', + 'description' => __('SMTP server hostname (e.g., smtp.gmail.com)', 'wp-digital-download') + ) + ); + + add_settings_field( + 'wpdd_smtp_port', + __('SMTP Port', 'wp-digital-download'), + array(__CLASS__, 'number_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_port', + 'description' => __('SMTP server port number (common ports: 25, 465, 587)', 'wp-digital-download'), + 'min' => 1, + 'max' => 65535 + ) + ); + + add_settings_field( + 'wpdd_smtp_encryption', + __('SMTP Encryption', 'wp-digital-download'), + array(__CLASS__, 'select_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_encryption', + 'options' => array( + '' => __('None', 'wp-digital-download'), + 'tls' => __('TLS', 'wp-digital-download'), + 'ssl' => __('SSL', 'wp-digital-download') + ), + 'description' => __('Select encryption method - TLS is recommended for most providers', 'wp-digital-download') + ) + ); + + add_settings_field( + 'wpdd_smtp_autodetect', + __('Auto-Detect Settings', 'wp-digital-download'), + array(__CLASS__, 'smtp_autodetect_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array() + ); + + add_settings_field( + 'wpdd_smtp_username', + __('SMTP Username', 'wp-digital-download'), + array(__CLASS__, 'text_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_username', + 'description' => __('SMTP authentication username', 'wp-digital-download') + ) + ); + + add_settings_field( + 'wpdd_smtp_password', + __('SMTP Password', 'wp-digital-download'), + array(__CLASS__, 'password_field'), + 'wpdd_settings', + 'wpdd_email_settings', + array( + 'name' => 'wpdd_smtp_password', + 'description' => __('SMTP authentication password', 'wp-digital-download') + ) + ); } private static function add_download_fields() { @@ -305,59 +412,101 @@ class WPDD_Settings { } public static function render_settings_page() { + $active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general'; ?>

-
-
-

-

-
    -
  1. -
  2. -
  3. -
  4. -
+ + +
+
+
+ +
-
-

-
    -
  • [wpdd_shop] -
  • -
  • [wpdd_customer_purchases] -
  • -
  • [wpdd_checkout] -
  • -
  • [wpdd_thank_you] -
  • -
  • [wpdd_product id="123"] -
  • -
-
- -
-

- +
+ +
+

+

+
    +
  1. +
  2. +
  3. +
  4. +
+
+ +
+

+
    +
  • [wpdd_shop] -
  • +
  • [wpdd_customer_purchases] -
  • +
  • [wpdd_checkout] -
  • +
  • [wpdd_thank_you] -
  • +
  • [wpdd_product id="123"] -
  • +
+
+ + +
+

+ +
- -
- -
@@ -584,9 +739,98 @@ class WPDD_Settings { )); } + public static function smtp_autodetect_field($args) { + ?> + + +

+ +

+ + + __('Plugin Constants', 'wp-digital-download'), + 'value' => __('Not Loaded', 'wp-digital-download'), + 'class' => 'wpdd-status-error' + ); + echo '
    '; + foreach ($status as $item) { + printf( + '
  • %s: %s
  • ', + esc_html($item['label']), + esc_attr($item['class']), + esc_html($item['value']) + ); + } + echo '
'; + return; + } + $upload_dir = wp_upload_dir(); $protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR; @@ -659,6 +903,73 @@ class WPDD_Settings { echo ''; } + public static function render_general_tab() { + ?> +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ '; + do_settings_fields('wpdd_settings', $section_id); + echo ''; + } + public static function sanitize_commission_rate($input) { $value = floatval($input); if ($value < 0) { diff --git a/admin/views/order-details.php b/admin/views/order-details.php new file mode 100644 index 0000000..a8c5ab7 --- /dev/null +++ b/admin/views/order-details.php @@ -0,0 +1,261 @@ + +
+

+ +
+ +
+

+ + + + + + + + + + + + + + + + + + + + transaction_id)) : ?> + + + + + + + + + + +
order_number); ?>
+ + status)); ?> + +
purchase_date))); ?>
+ 'PayPal', + 'free' => 'Free Download', + 'manual' => 'Manual Payment' + ); + echo esc_html($payment_methods[$order->payment_method] ?? ucfirst($order->payment_method)); + ?> +
transaction_id); ?>
+ + amount, $order->currency); ?> + +
+
+ +
+

+ + + + + + + + + + + + customer_id > 0) : ?> + + + + + + +
customer_name); ?>
+ + customer_email); ?> + +
+ + + +
+
+ +
+

+ + + + + + + + + + + + creator_id > 0) : ?> + + + + + + +
+ + product_name); ?> + +
product_id); ?>
+ creator_id); + if ($creator) : ?> + + display_name); ?> + + + + +
+
+ + get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_download_links WHERE order_id = %d ORDER BY created_at DESC", + $order->id + )); + ?> + + + + + + get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_downloads WHERE order_id = %d ORDER BY download_date DESC LIMIT 20", + $order->id + )); + ?> + + +
+

+ () +

+ + + + + + + + + + + + + + + + + + +
download_date))); ?>ip_address); ?> + user_agent, 10)); ?> +
+
+ + +
+ + + + + status === 'completed') : ?> + + + + +
+ +
+
+ + \ No newline at end of file diff --git a/assets/js/admin-payouts.js b/assets/js/admin-payouts.js new file mode 100644 index 0000000..a5447b3 --- /dev/null +++ b/assets/js/admin-payouts.js @@ -0,0 +1,124 @@ +jQuery(document).ready(function($) { + // Handle individual payout processing + $('.wpdd-process-payout').on('click', function(e) { + e.preventDefault(); + + var $button = $(this); + var payoutId = $button.data('payout-id'); + var originalText = $button.text(); + + // Disable button and show loading + $button.prop('disabled', true).text('Processing...'); + + $.ajax({ + url: ajaxurl, + type: 'POST', + data: { + action: 'wpdd_process_payout', + payout_id: payoutId, + nonce: wpdd_admin_nonce + }, + success: function(response) { + if (response.success) { + $button.closest('tr').find('.payout-status').text('Processing'); + $button.text('Processed').removeClass('wpdd-process-payout').addClass('button-disabled'); + } else { + alert('Error: ' + (response.data || 'Unknown error occurred')); + $button.prop('disabled', false).text(originalText); + } + }, + error: function() { + alert('Ajax request failed'); + $button.prop('disabled', false).text(originalText); + } + }); + }); + + // Handle payout request approval + $('.wpdd-approve-request').on('click', function(e) { + e.preventDefault(); + + var $button = $(this); + var requestId = $button.data('request-id'); + var originalText = $button.text(); + + // Disable button and show loading + $button.prop('disabled', true).text('Processing...'); + + $.ajax({ + url: ajaxurl, + type: 'POST', + data: { + action: 'wpdd_approve_payout_request', + request_id: requestId, + nonce: wpdd_admin_nonce + }, + success: function(response) { + if (response.success) { + location.reload(); // Reload to show updated status + } else { + alert('Error: ' + (response.data || 'Unknown error occurred')); + $button.prop('disabled', false).text(originalText); + } + }, + error: function() { + alert('Ajax request failed'); + $button.prop('disabled', false).text(originalText); + } + }); + }); + + // Handle bulk payout processing + $('#wpdd-process-bulk-payouts').on('click', function(e) { + e.preventDefault(); + + var checkedPayouts = $('.wpdd-payout-checkbox:checked'); + + if (checkedPayouts.length === 0) { + alert('Please select at least one payout to process.'); + return; + } + + if (!confirm('Are you sure you want to process ' + checkedPayouts.length + ' payout(s)?')) { + return; + } + + var $button = $(this); + var originalText = $button.text(); + var payoutIds = []; + + checkedPayouts.each(function() { + payoutIds.push($(this).val()); + }); + + // Disable button and show loading + $button.prop('disabled', true).text('Processing...'); + + $.ajax({ + url: ajaxurl, + type: 'POST', + data: { + action: 'wpdd_process_bulk_payouts', + payout_ids: payoutIds, + nonce: wpdd_admin_nonce + }, + success: function(response) { + if (response.success) { + location.reload(); // Reload to show updated status + } else { + alert('Error: ' + (response.data || 'Unknown error occurred')); + $button.prop('disabled', false).text(originalText); + } + }, + error: function() { + alert('Ajax request failed'); + $button.prop('disabled', false).text(originalText); + } + }); + }); + + // Handle select all checkboxes + $('#wpdd-select-all-payouts').on('change', function() { + $('.wpdd-payout-checkbox').prop('checked', $(this).prop('checked')); + }); +}); \ No newline at end of file diff --git a/docs/developer-integration-guide.md b/docs/developer-integration-guide.md new file mode 100644 index 0000000..2f1e439 --- /dev/null +++ b/docs/developer-integration-guide.md @@ -0,0 +1,420 @@ +# WP Digital Download - Software Licensing Integration Guide + +This guide shows developers how to integrate their WordPress plugins with the WP Digital Download licensing and update system. + +## Quick Start + +### 1. Download the Integration Library + +Download `wpdd-plugin-updater.php` from your product page and include it in your plugin. + +### 2. Basic Integration + +```php +init_updater(); + } + + private function init_updater() { + // Only initialize updater in admin area + if (!is_admin()) { + return; + } + + $license_key = get_option('my_plugin_license_key', ''); + + $this->updater = new WPDD_Plugin_Updater( + __FILE__, // Main plugin file + $license_key, // License key from user + 'https://your-store.com', // Your store URL + array( + 'add_settings_page' => true // Add license settings page + ) + ); + } +} + +new My_Awesome_Plugin(); +``` + +## Advanced Integration + +### Custom License Settings Page + +If you want to integrate license management into your existing settings: + +```php +class My_Plugin_Settings { + + private $updater; + + public function __construct() { + $this->init_updater(); + add_action('admin_menu', array($this, 'add_settings_page')); + add_action('admin_init', array($this, 'handle_license_actions')); + } + + private function init_updater() { + $license_key = get_option('my_plugin_license_key', ''); + + $this->updater = new WPDD_Plugin_Updater( + MY_PLUGIN_FILE, + $license_key, + 'https://your-store.com', + array('add_settings_page' => false) // We'll handle settings ourselves + ); + } + + public function handle_license_actions() { + if (isset($_POST['activate_license'])) { + $license_key = sanitize_text_field($_POST['license_key']); + $result = $this->updater->activate_license($license_key); + + if ($result['success']) { + update_option('my_plugin_license_key', $license_key); + add_settings_error('my_plugin', 'activated', 'License activated!', 'updated'); + } else { + add_settings_error('my_plugin', 'error', $result['message'], 'error'); + } + } + + if (isset($_POST['deactivate_license'])) { + $result = $this->updater->deactivate_license(); + if ($result['success']) { + delete_option('my_plugin_license_key'); + add_settings_error('my_plugin', 'deactivated', 'License deactivated!', 'updated'); + } + } + } + + public function render_license_section() { + $license_key = get_option('my_plugin_license_key', ''); + $is_valid = $this->updater->validate_license(); + + ?> +

License Settings

+ + + + + +
+ + + + ✓ Active + + ✗ Invalid + + +

+ Enter your license key to receive automatic updates. +

+
+ + +

+ +

+ +

+ +

+ + updater = new WPDD_Plugin_Updater( + MY_PLUGIN_FILE, + $license_key, + 'https://your-store.com' + ); + + // Only enable premium features if license is valid + if ($this->is_license_valid()) { + $this->enable_premium_features(); + } else { + $this->show_license_notice(); + } + } + + private function is_license_valid() { + return $this->updater->validate_license(); + } + + private function enable_premium_features() { + // Add your premium functionality here + add_action('init', array($this, 'init_premium_features')); + } + + private function show_license_notice() { + add_action('admin_notices', function() { + ?> +
+

+ My Awesome Plugin: + Please + activate your license to access premium features and receive updates. +

+
+ validate_license(); +// Returns: boolean +``` + +##### activate_license($license_key) +Activates a license key for the current site. +```php +$result = $updater->activate_license('XXXX-XXXX-XXXX-XXXX'); +// Returns: array with 'success', 'message', and additional data +``` + +##### deactivate_license() +Deactivates the current license from this site. +```php +$result = $updater->deactivate_license(); +// Returns: array with 'success' and 'message' +``` + +## Repository Setup (For Store Owners) + +### 1. Create Software Product + +1. Go to your WordPress admin → Digital Products → Add New Product +2. Select "Software License" as product type +3. Fill in the software licensing fields: + - Git Repository URL + - License settings (max activations, duration) + - Version information + +### 2. Configure Git Webhook + +Add the generated webhook URL to your repository settings. The system receives webhook notifications FROM your Git platform when releases are published: + +**Gitea:** +1. Go to Settings → Webhooks +2. Add webhook with the URL from your product page +3. Set Content-Type to `application/json` +4. Select "Release events" as the trigger +5. Ensure webhook is active + +**GitHub:** +1. Go to Settings → Webhooks +2. Add webhook with the URL from your product page +3. Set Content-Type to `application/json` +4. Select "Releases" events (or "Just the push event" for tag-based releases) + +**GitLab:** +1. Go to Settings → Webhooks +2. Add the webhook URL +3. Select "Tag push events" or "Releases events" + +### 3. Release Process + +**Option 1: Using Git Platform Releases (Recommended for Gitea/GitHub)** +1. Create a release through your Git platform's web interface: + - Navigate to Releases section + - Click "Create Release" or "New Release" + - Set tag name (e.g., `v1.2.0`) + - Add release notes in the description + - Publish the release + +2. The webhook automatically receives the release notification and: + - Detects the new version from the release + - Clones the repository at the specific tag + - Creates distribution packages (removes dev files, creates ZIP) + - Stores version info and changelog in the database + - Makes update available to customers with active licenses + +**Option 2: Using Git Tags (Alternative)** +1. Create and push a git tag: + ```bash + git tag -a v1.2.0 -m "Version 1.2.0" + git push origin v1.2.0 + ``` + +2. The webhook receives the tag push notification and processes the release similarly + +## API Endpoints + +### License Validation +``` +POST /wp-json/wpdd/v1/validate-license +Body: { + "license_key": "XXXX-XXXX-XXXX-XXXX", + "product_slug": "my-plugin", + "site_url": "https://example.com" +} +``` + +### Update Check +``` +GET /wp-json/wpdd/v1/check-update/my-plugin?license_key=XXXX&version=1.0.0 +``` + +### Download Update +``` +GET /wp-json/wpdd/v1/download-update/my-plugin?license_key=XXXX +``` + +## Testing Your Integration + +### 1. Local Testing + +```php +// Add this to your plugin for testing +if (defined('WP_DEBUG') && WP_DEBUG) { + add_action('admin_notices', function() { + $license_key = get_option('my_plugin_license_key', ''); + $updater = new WPDD_Plugin_Updater(__FILE__, $license_key, 'https://your-store.com'); + $is_valid = $updater->validate_license(); + + echo '
'; + echo '

License Status: ' . ($is_valid ? 'Valid' : 'Invalid') . '

'; + echo '
'; + }); +} +``` + +### 2. Force Update Check + +```php +// Add this temporarily to force update check +add_action('admin_init', function() { + if (isset($_GET['force_update_check'])) { + delete_transient('wpdd_update_my-plugin'); + delete_site_transient('update_plugins'); + wp_redirect(admin_url('plugins.php')); + exit; + } +}); +``` + +Then visit: `wp-admin/plugins.php?force_update_check=1` + +## Best Practices + +### 1. Error Handling +Always handle API failures gracefully: + +```php +$result = $updater->validate_license(); +if ($result === false) { + // Network error or server down - allow functionality to continue + // but maybe show a notice +} +``` + +### 2. Caching +The updater automatically caches responses. Don't call validation on every page load: + +```php +// Good - check once per day +$last_check = get_option('my_plugin_license_check', 0); +if (time() - $last_check > DAY_IN_SECONDS) { + $is_valid = $updater->validate_license(); + update_option('my_plugin_license_check', time()); + update_option('my_plugin_license_valid', $is_valid); +} else { + $is_valid = get_option('my_plugin_license_valid', false); +} +``` + +### 3. Graceful Degradation +Design your plugin to work without a valid license, but with reduced functionality: + +```php +if ($this->is_license_valid()) { + // Full functionality + $this->enable_all_features(); +} else { + // Basic functionality only + $this->enable_basic_features(); + $this->show_upgrade_notice(); +} +``` + +## Troubleshooting + +### Common Issues + +1. **Updates not showing:** Check that the plugin slug matches the product slug in your store +2. **License validation fails:** Ensure the update server URL is correct and accessible +3. **Download fails:** Verify the license is activated and not expired + +### Debug Mode + +Enable WordPress debug logging and check for WPDD Updater messages: + +```php +// wp-config.php +define('WP_DEBUG', true); +define('WP_DEBUG_LOG', true); +``` + +Check `/wp-content/debug.log` for error messages. + +## Support + +For integration support: +- Check the troubleshooting section above +- Enable debug logging and check for error messages +- Contact support with your store URL and plugin details + +## Example Files + +Complete example plugins are available in the `/examples/` directory of this package. \ No newline at end of file diff --git a/includes/class-wpdd-api.php b/includes/class-wpdd-api.php new file mode 100644 index 0000000..818d088 --- /dev/null +++ b/includes/class-wpdd-api.php @@ -0,0 +1,785 @@ + '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 + $filename = basename($package_path); + 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); + + // 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 + } +} \ No newline at end of file diff --git a/includes/class-wpdd-install.php b/includes/class-wpdd-install.php index 19302f5..15abe82 100644 --- a/includes/class-wpdd-install.php +++ b/includes/class-wpdd-install.php @@ -20,9 +20,13 @@ class WPDD_Install { WPDD_Post_Types::register_taxonomies(); self::create_tables(); - self::create_pages(); self::create_upload_protection(); + // Set flag to show setup notice instead of creating pages automatically + if (!get_option('wpdd_setup_completed')) { + add_option('wpdd_show_setup_notice', true); + } + WPDD_Roles::create_roles(); // Flush rewrite rules after post types are registered @@ -126,6 +130,96 @@ class WPDD_Install { KEY transaction_id (transaction_id) ) $charset_collate;"; + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_balance_adjustments ( + id bigint(20) NOT NULL AUTO_INCREMENT, + creator_id bigint(20) NOT NULL, + adjustment_type varchar(20) NOT NULL, + amount decimal(10,2) NOT NULL, + previous_balance decimal(10,2) NOT NULL, + new_balance decimal(10,2) NOT NULL, + reason text NOT NULL, + adjusted_by bigint(20) NOT NULL, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY creator_id (creator_id), + KEY adjusted_by (adjusted_by) + ) $charset_collate;"; + + // Software Licensing Tables + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_licenses ( + id bigint(20) NOT NULL AUTO_INCREMENT, + license_key varchar(64) NOT NULL, + product_id bigint(20) NOT NULL, + order_id bigint(20) NOT NULL, + customer_id bigint(20) NOT NULL, + customer_email varchar(100) NOT NULL, + status varchar(20) NOT NULL DEFAULT 'active', + activations_count int(11) DEFAULT 0, + max_activations int(11) DEFAULT 1, + expires_at datetime DEFAULT NULL, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + last_checked datetime DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY license_key (license_key), + KEY product_id (product_id), + KEY order_id (order_id), + KEY customer_id (customer_id), + KEY status (status) + ) $charset_collate;"; + + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_license_activations ( + id bigint(20) NOT NULL AUTO_INCREMENT, + license_id bigint(20) NOT NULL, + license_key varchar(64) NOT NULL, + site_url varchar(255) NOT NULL, + site_name varchar(255) DEFAULT NULL, + activated_at datetime DEFAULT CURRENT_TIMESTAMP, + last_checked datetime DEFAULT NULL, + wp_version varchar(20) DEFAULT NULL, + php_version varchar(20) DEFAULT NULL, + status varchar(20) NOT NULL DEFAULT 'active', + PRIMARY KEY (id), + KEY license_id (license_id), + KEY license_key (license_key), + KEY site_url (site_url), + KEY status (status) + ) $charset_collate;"; + + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_software_versions ( + id bigint(20) NOT NULL AUTO_INCREMENT, + product_id bigint(20) NOT NULL, + version varchar(20) NOT NULL, + changelog text DEFAULT NULL, + release_notes text DEFAULT NULL, + download_url text DEFAULT NULL, + package_url text DEFAULT NULL, + min_wp_version varchar(20) DEFAULT NULL, + tested_wp_version varchar(20) DEFAULT NULL, + min_php_version varchar(20) DEFAULT NULL, + release_date datetime DEFAULT CURRENT_TIMESTAMP, + git_tag varchar(100) DEFAULT NULL, + git_commit varchar(100) DEFAULT NULL, + PRIMARY KEY (id), + KEY product_id (product_id), + KEY version (version), + KEY release_date (release_date) + ) $charset_collate;"; + + $sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_webhook_events ( + id bigint(20) NOT NULL AUTO_INCREMENT, + product_id bigint(20) NOT NULL, + event_type varchar(50) NOT NULL, + payload text DEFAULT NULL, + processed varchar(20) NOT NULL DEFAULT 'pending', + error_message text DEFAULT NULL, + received_at datetime DEFAULT CURRENT_TIMESTAMP, + processed_at datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY product_id (product_id), + KEY processed (processed), + KEY received_at (received_at) + ) $charset_collate;"; + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); foreach ($sql as $query) { @@ -135,6 +229,10 @@ class WPDD_Install { update_option('wpdd_db_version', WPDD_VERSION); } + public static function create_pages_optional() { + return self::create_pages(); + } + private static function create_pages() { $pages = array( 'shop' => array( @@ -159,6 +257,8 @@ class WPDD_Install { ) ); + $created_pages = array(); + foreach ($pages as $slug => $page) { // Check if page already exists $existing_page_id = get_option($page['option']); @@ -184,8 +284,11 @@ class WPDD_Install { if ($page_id && !is_wp_error($page_id)) { update_option($page['option'], $page_id); + $created_pages[] = $page_id; } } + + return $created_pages; } private static function create_upload_protection() { diff --git a/includes/class-wpdd-license-manager.php b/includes/class-wpdd-license-manager.php new file mode 100644 index 0000000..1ed1a31 --- /dev/null +++ b/includes/class-wpdd-license-manager.php @@ -0,0 +1,399 @@ +product_id, '_wpdd_product_type', true); + if ($product_type !== 'software_license') { + return; + } + + // Check if license already exists for this order + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}wpdd_licenses WHERE order_id = %d", + $order_id + )); + + if ($existing) { + return; + } + + // Generate unique license key + $license_key = self::generate_license_key(); + + // Ensure it's unique + while (self::license_key_exists($license_key)) { + $license_key = self::generate_license_key(); + } + + // Get license settings from product + $max_activations = get_post_meta($order->product_id, '_wpdd_max_activations', true) ?: 1; + $license_duration = get_post_meta($order->product_id, '_wpdd_license_duration', true); // in days + + $expires_at = null; + if ($license_duration && $license_duration > 0) { + $expires_at = date('Y-m-d H:i:s', strtotime("+{$license_duration} days")); + } + + // Insert license + $wpdb->insert( + $wpdb->prefix . 'wpdd_licenses', + array( + 'license_key' => $license_key, + 'product_id' => $order->product_id, + 'order_id' => $order_id, + 'customer_id' => $order->customer_id, + 'customer_email' => $order->customer_email, + 'status' => 'active', + 'max_activations' => $max_activations, + 'expires_at' => $expires_at, + 'created_at' => current_time('mysql') + ), + array('%s', '%d', '%d', '%d', '%s', '%s', '%d', '%s', '%s') + ); + + // Send license key to customer + self::send_license_email($order, $license_key); + + return $license_key; + } + + /** + * Check if license key exists + */ + public static function license_key_exists($license_key) { + global $wpdb; + return $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s", + $license_key + )) > 0; + } + + /** + * Validate license key + */ + public static function validate_license($license_key, $product_slug = null, $site_url = null) { + global $wpdb; + + // Get license details + $license = $wpdb->get_row($wpdb->prepare( + "SELECT l.*, p.post_name as product_slug + FROM {$wpdb->prefix}wpdd_licenses l + LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID + WHERE l.license_key = %s", + $license_key + )); + + if (!$license) { + return array( + 'valid' => false, + 'error' => 'invalid_license', + 'message' => __('Invalid license key.', 'wp-digital-download') + ); + } + + // Check product match + if ($product_slug && $license->product_slug !== $product_slug) { + return array( + 'valid' => false, + 'error' => 'product_mismatch', + 'message' => __('License key is not valid for this product.', 'wp-digital-download') + ); + } + + // Check status + if ($license->status !== 'active') { + return array( + 'valid' => false, + 'error' => 'license_' . $license->status, + 'message' => sprintf(__('License is %s.', 'wp-digital-download'), $license->status) + ); + } + + // Check expiration + if ($license->expires_at && strtotime($license->expires_at) < time()) { + // Update status to expired + $wpdb->update( + $wpdb->prefix . 'wpdd_licenses', + array('status' => 'expired'), + array('id' => $license->id), + array('%s'), + array('%d') + ); + + return array( + 'valid' => false, + 'error' => 'license_expired', + 'message' => __('License has expired.', 'wp-digital-download'), + 'expired_at' => $license->expires_at + ); + } + + // Check activation limit if site_url provided + if ($site_url) { + $activation_count = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d AND status = 'active'", + $license->id + )); + + $is_activated = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d AND site_url = %s AND status = 'active'", + $license->id, + $site_url + )); + + if (!$is_activated && $activation_count >= $license->max_activations) { + return array( + 'valid' => false, + 'error' => 'activation_limit', + 'message' => sprintf(__('License activation limit reached (%d/%d).', 'wp-digital-download'), + $activation_count, $license->max_activations), + 'activations' => $activation_count, + 'max_activations' => $license->max_activations + ); + } + } + + // Update last checked + $wpdb->update( + $wpdb->prefix . 'wpdd_licenses', + array('last_checked' => current_time('mysql')), + array('id' => $license->id), + array('%s'), + array('%d') + ); + + return array( + 'valid' => true, + 'license' => $license, + 'message' => __('License is valid.', 'wp-digital-download') + ); + } + + /** + * Activate license for a site + */ + public static function activate_license($license_key, $site_url, $site_name = null, $wp_version = null, $php_version = null) { + global $wpdb; + + // Validate license first + $validation = self::validate_license($license_key, null, $site_url); + if (!$validation['valid']) { + return $validation; + } + + $license = $validation['license']; + + // Check if already activated for this site + $existing = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d AND site_url = %s", + $license->id, + $site_url + )); + + if ($existing && $existing->status === 'active') { + return array( + 'success' => true, + 'already_active' => true, + 'message' => __('License already activated for this site.', 'wp-digital-download') + ); + } + + if ($existing) { + // Reactivate + $wpdb->update( + $wpdb->prefix . 'wpdd_license_activations', + array( + 'status' => 'active', + 'activated_at' => current_time('mysql'), + 'last_checked' => current_time('mysql'), + 'wp_version' => $wp_version, + 'php_version' => $php_version, + 'site_name' => $site_name + ), + array('id' => $existing->id), + array('%s', '%s', '%s', '%s', '%s', '%s'), + array('%d') + ); + } else { + // New activation + $wpdb->insert( + $wpdb->prefix . 'wpdd_license_activations', + array( + 'license_id' => $license->id, + 'license_key' => $license_key, + 'site_url' => $site_url, + 'site_name' => $site_name, + 'activated_at' => current_time('mysql'), + 'last_checked' => current_time('mysql'), + 'wp_version' => $wp_version, + 'php_version' => $php_version, + 'status' => 'active' + ), + array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') + ); + } + + // Update activation count + $count = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d AND status = 'active'", + $license->id + )); + + $wpdb->update( + $wpdb->prefix . 'wpdd_licenses', + array('activations_count' => $count), + array('id' => $license->id), + array('%d'), + array('%d') + ); + + return array( + 'success' => true, + 'message' => __('License activated successfully.', 'wp-digital-download'), + 'activations' => $count, + 'max_activations' => $license->max_activations + ); + } + + /** + * Deactivate license for a site + */ + public static function deactivate_license($license_key, $site_url) { + global $wpdb; + + // Get license + $license = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s", + $license_key + )); + + if (!$license) { + return array( + 'success' => false, + 'error' => 'invalid_license', + 'message' => __('Invalid license key.', 'wp-digital-download') + ); + } + + // Deactivate + $updated = $wpdb->update( + $wpdb->prefix . 'wpdd_license_activations', + array('status' => 'deactivated'), + array( + 'license_id' => $license->id, + 'site_url' => $site_url + ), + array('%s'), + array('%d', '%s') + ); + + if (!$updated) { + return array( + 'success' => false, + 'error' => 'not_activated', + 'message' => __('License not activated for this site.', 'wp-digital-download') + ); + } + + // Update activation count + $count = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d AND status = 'active'", + $license->id + )); + + $wpdb->update( + $wpdb->prefix . 'wpdd_licenses', + array('activations_count' => $count), + array('id' => $license->id), + array('%d'), + array('%d') + ); + + return array( + 'success' => true, + 'message' => __('License deactivated successfully.', 'wp-digital-download') + ); + } + + /** + * Send license email to customer + */ + private static function send_license_email($order, $license_key) { + $product = get_post($order->product_id); + $subject = sprintf(__('Your License Key for %s', 'wp-digital-download'), $product->post_title); + + $message = sprintf( + __("Hi %s,\n\nThank you for your purchase!\n\nHere is your license key for %s:\n\n%s\n\nPlease keep this key safe. You will need it to activate and receive updates for your software.\n\nBest regards,\n%s", 'wp-digital-download'), + $order->customer_name ?: $order->customer_email, + $product->post_title, + $license_key, + get_bloginfo('name') + ); + + wp_mail($order->customer_email, $subject, $message); + } + + /** + * Get license details for admin + */ + public static function get_license_details($license_key) { + global $wpdb; + + $license = $wpdb->get_row($wpdb->prepare( + "SELECT l.*, p.post_title as product_name + FROM {$wpdb->prefix}wpdd_licenses l + LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID + WHERE l.license_key = %s", + $license_key + )); + + if (!$license) { + return null; + } + + // Get activations + $license->activations = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}wpdd_license_activations + WHERE license_id = %d + ORDER BY activated_at DESC", + $license->id + )); + + return $license; + } +} \ No newline at end of file diff --git a/includes/class-wpdd-metaboxes.php b/includes/class-wpdd-metaboxes.php index b4862be..13513b2 100644 --- a/includes/class-wpdd-metaboxes.php +++ b/includes/class-wpdd-metaboxes.php @@ -12,6 +12,15 @@ class WPDD_Metaboxes { } public static function add_meta_boxes() { + add_meta_box( + 'wpdd_product_type', + __('Product Type', 'wp-digital-download'), + array(__CLASS__, 'render_product_type_metabox'), + 'wpdd_product', + 'side', + 'high' + ); + add_meta_box( 'wpdd_product_pricing', __('Product Pricing', 'wp-digital-download'), @@ -30,6 +39,15 @@ class WPDD_Metaboxes { 'high' ); + add_meta_box( + 'wpdd_software_licensing', + __('Software Licensing', 'wp-digital-download'), + array(__CLASS__, 'render_software_licensing_metabox'), + 'wpdd_product', + 'normal', + 'high' + ); + add_meta_box( 'wpdd_product_settings', __('Product Settings', 'wp-digital-download'), @@ -78,6 +96,46 @@ class WPDD_Metaboxes { ID, '_wpdd_product_type', true) ?: 'digital_download'; + ?> +

+ +
+ +

+

+ +
+ +

+ + + ID, '_wpdd_files', true); if (!is_array($files)) { @@ -148,6 +206,101 @@ class WPDD_Metaboxes { ID, '_wpdd_git_repository', true); + $git_username = get_post_meta($post->ID, '_wpdd_git_username', true); + $git_token = get_post_meta($post->ID, '_wpdd_git_token', true); + $webhook_passcode = get_post_meta($post->ID, '_wpdd_webhook_passcode', true); + $max_activations = get_post_meta($post->ID, '_wpdd_max_activations', true) ?: 1; + $license_duration = get_post_meta($post->ID, '_wpdd_license_duration', true); + $current_version = get_post_meta($post->ID, '_wpdd_current_version', true); + $min_wp_version = get_post_meta($post->ID, '_wpdd_min_wp_version', true); + $tested_wp_version = get_post_meta($post->ID, '_wpdd_tested_wp_version', true); + + // Generate webhook passcode if not set + if (!$webhook_passcode) { + $webhook_passcode = wp_generate_password(32, false); + update_post_meta($post->ID, '_wpdd_webhook_passcode', $webhook_passcode); + } + + $webhook_url = home_url("/wp-json/wpdd/v1/webhook/{$post->ID}/{$webhook_passcode}"); + ?> +
+

+

+
+ + +

+ +

+
+ + +

+ +

+
+ + +

+ +

+

+
+ + +

+ +

+ +

+ +

+

+
+ + +

+ +

+
+ + +

+ +

+

+
+ +

+ +

+
+ +

+ +

+
+ +

+
+ + + ID, '_wpdd_download_limit', true); $download_expiry = get_post_meta($post->ID, '_wpdd_download_expiry', true); @@ -304,5 +457,52 @@ class WPDD_Metaboxes { update_post_meta($post_id, '_wpdd_watermark_text', sanitize_text_field($_POST['wpdd_watermark_text'])); } + + // Product type + if (isset($_POST['wpdd_product_type'])) { + update_post_meta($post_id, '_wpdd_product_type', + sanitize_text_field($_POST['wpdd_product_type'])); + } + + // Software licensing fields + if (isset($_POST['wpdd_git_repository'])) { + update_post_meta($post_id, '_wpdd_git_repository', + esc_url_raw($_POST['wpdd_git_repository'])); + } + + if (isset($_POST['wpdd_git_username'])) { + update_post_meta($post_id, '_wpdd_git_username', + sanitize_text_field($_POST['wpdd_git_username'])); + } + + if (isset($_POST['wpdd_git_token'])) { + update_post_meta($post_id, '_wpdd_git_token', + sanitize_text_field($_POST['wpdd_git_token'])); + } + + if (isset($_POST['wpdd_max_activations'])) { + update_post_meta($post_id, '_wpdd_max_activations', + intval($_POST['wpdd_max_activations'])); + } + + if (isset($_POST['wpdd_license_duration'])) { + update_post_meta($post_id, '_wpdd_license_duration', + intval($_POST['wpdd_license_duration'])); + } + + if (isset($_POST['wpdd_current_version'])) { + update_post_meta($post_id, '_wpdd_current_version', + sanitize_text_field($_POST['wpdd_current_version'])); + } + + if (isset($_POST['wpdd_min_wp_version'])) { + update_post_meta($post_id, '_wpdd_min_wp_version', + sanitize_text_field($_POST['wpdd_min_wp_version'])); + } + + if (isset($_POST['wpdd_tested_wp_version'])) { + update_post_meta($post_id, '_wpdd_tested_wp_version', + sanitize_text_field($_POST['wpdd_tested_wp_version'])); + } } } \ No newline at end of file diff --git a/includes/class-wpdd-paypal.php b/includes/class-wpdd-paypal.php index a7327cf..deff6c8 100644 --- a/includes/class-wpdd-paypal.php +++ b/includes/class-wpdd-paypal.php @@ -15,7 +15,26 @@ class WPDD_PayPal { } public static function enqueue_paypal_sdk() { - if (!is_page(get_option('wpdd_checkout_page_id'))) { + // 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; } @@ -183,6 +202,9 @@ class WPDD_PayPal { $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); diff --git a/includes/class-wpdd-setup.php b/includes/class-wpdd-setup.php new file mode 100644 index 0000000..49a91ac --- /dev/null +++ b/includes/class-wpdd-setup.php @@ -0,0 +1,169 @@ +id, 'wpdd_product') === false && $screen->id !== 'dashboard') { + return; + } + + ?> +
+

+

+ +
+ + + + + + + +
+ +
+

+
    +
  • -
  • +
  • -
  • +
  • -
  • +
  • -
  • +
+
+ + +
+ + + $page->post_title, + 'edit_url' => admin_url('post.php?post=' . $page_id . '&action=edit'), + 'view_url' => get_permalink($page_id) + ); + } + } + + update_option('wpdd_setup_completed', true); + delete_option('wpdd_show_setup_notice'); + + wp_send_json_success(array( + 'message' => sprintf(__('%d pages created successfully!', 'wp-digital-download'), count($pages_info)), + 'pages' => $pages_info + )); + } else { + wp_send_json_error(__('No pages were created. They may already exist.', 'wp-digital-download')); + } + } + + public static function dismiss_setup_notice() { + check_ajax_referer('wpdd_setup_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(__('Permission denied.', 'wp-digital-download')); + } + + update_option('wpdd_setup_completed', true); + delete_option('wpdd_show_setup_notice'); + + wp_send_json_success(); + } +} \ No newline at end of file diff --git a/includes/wpdd-plugin-updater.php b/includes/wpdd-plugin-updater.php new file mode 100644 index 0000000..44c88f0 --- /dev/null +++ b/includes/wpdd-plugin-updater.php @@ -0,0 +1,471 @@ +plugin_file = $plugin_file; + $this->plugin_slug = basename($plugin_file, '.php'); + $this->license_key = $license_key; + $this->update_server = trailingslashit($update_server); + + // Get plugin version from header + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + $plugin_data = get_plugin_data($plugin_file); + $this->version = $plugin_data['Version']; + + $this->transient_key = 'wpdd_update_' . $this->plugin_slug; + + // Initialize hooks + $this->init_hooks(); + + // Add settings page if requested + if (isset($args['add_settings_page']) && $args['add_settings_page']) { + $this->add_settings_page(); + } + } + + /** + * Initialize WordPress hooks + */ + private function init_hooks() { + add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update')); + add_filter('plugins_api', array($this, 'plugin_info'), 10, 3); + add_filter('upgrader_pre_download', array($this, 'maybe_download_package'), 10, 3); + + // Clean up transients on plugin activation/deactivation + register_activation_hook($this->plugin_file, array($this, 'delete_transients')); + register_deactivation_hook($this->plugin_file, array($this, 'delete_transients')); + } + + /** + * Check for plugin updates + */ + public function check_for_update($transient) { + if (empty($transient->checked)) { + return $transient; + } + + // Get cached update info + $update_cache = get_transient($this->transient_key); + if ($update_cache !== false) { + if (isset($update_cache->update_available) && $update_cache->update_available) { + $transient->response[$this->plugin_file] = $update_cache; + } + return $transient; + } + + // Check for update from server + $update_info = $this->request_update_info(); + + if ($update_info && isset($update_info['update_available']) && $update_info['update_available']) { + $plugin_data = array( + 'slug' => $this->plugin_slug, + 'plugin' => $this->plugin_file, + 'new_version' => $update_info['version'], + 'url' => $update_info['url'], + 'package' => $update_info['package'], + 'tested' => $update_info['tested'], + 'requires' => $update_info['requires'], + 'requires_php' => $update_info['requires_php'], + 'compatibility' => new stdClass() + ); + + $update_cache = (object) $plugin_data; + $update_cache->update_available = true; + + // Cache for 12 hours + set_transient($this->transient_key, $update_cache, 12 * HOUR_IN_SECONDS); + + $transient->response[$this->plugin_file] = $update_cache; + } else { + // No update available - cache negative result for 12 hours + $update_cache = new stdClass(); + $update_cache->update_available = false; + set_transient($this->transient_key, $update_cache, 12 * HOUR_IN_SECONDS); + } + + return $transient; + } + + /** + * Provide plugin information for the update screen + */ + public function plugin_info($false, $action, $args) { + if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) { + return $false; + } + + $update_info = $this->request_update_info(); + + if (!$update_info) { + return $false; + } + + return (object) array( + 'slug' => $this->plugin_slug, + 'name' => $update_info['name'] ?? $this->plugin_slug, + 'version' => $update_info['version'] ?? $this->version, + 'author' => $update_info['author'] ?? '', + 'homepage' => $update_info['url'] ?? '', + 'requires' => $update_info['requires'] ?? '5.0', + 'tested' => $update_info['tested'] ?? get_bloginfo('version'), + 'requires_php' => $update_info['requires_php'] ?? '7.0', + 'download_link' => $update_info['package'] ?? '', + 'sections' => array( + 'changelog' => $update_info['changelog'] ?? '', + 'description' => $update_info['description'] ?? '' + ), + 'banners' => array(), + 'icons' => array() + ); + } + + /** + * Handle package download with license validation + */ + public function maybe_download_package($reply, $package, $upgrader) { + // Check if this is our plugin's package + if (strpos($package, $this->update_server) === false || strpos($package, $this->plugin_slug) === false) { + return $reply; + } + + // Validate license before download + $license_valid = $this->validate_license(); + if (!$license_valid) { + return new WP_Error('license_invalid', __('Your license key is invalid or expired. Please update your license key.')); + } + + return $reply; + } + + /** + * Request update information from server + */ + private function request_update_info() { + $url = $this->update_server . "wp-json/wpdd/v1/check-update/{$this->plugin_slug}"; + $url = add_query_arg(array( + 'license_key' => $this->license_key, + 'version' => $this->version, + 'site_url' => home_url() + ), $url); + + $response = wp_remote_get($url, array( + 'timeout' => 15, + 'headers' => array( + 'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url() + ) + )); + + if (is_wp_error($response)) { + error_log('WPDD Updater: Failed to check for updates - ' . $response->get_error_message()); + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (!$data || !isset($data['success'])) { + error_log('WPDD Updater: Invalid response from update server'); + return false; + } + + if (!$data['success']) { + if (isset($data['error'])) { + error_log('WPDD Updater: ' . $data['error'] . ' - ' . ($data['message'] ?? '')); + } + return false; + } + + return $data; + } + + /** + * Validate license with server + */ + public function validate_license() { + if (empty($this->license_key)) { + return false; + } + + $url = $this->update_server . 'wp-json/wpdd/v1/validate-license'; + + $response = wp_remote_post($url, array( + 'timeout' => 15, + 'body' => array( + 'license_key' => $this->license_key, + 'product_slug' => $this->plugin_slug, + 'site_url' => home_url() + ), + 'headers' => array( + 'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url() + ) + )); + + if (is_wp_error($response)) { + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + return $data && isset($data['success']) && $data['success']; + } + + /** + * Activate license + */ + public function activate_license($license_key = null) { + if ($license_key) { + $this->license_key = $license_key; + } + + if (empty($this->license_key)) { + return array( + 'success' => false, + 'message' => __('Please enter a license key.') + ); + } + + $url = $this->update_server . 'wp-json/wpdd/v1/activate-license'; + + $response = wp_remote_post($url, array( + 'timeout' => 15, + 'body' => array( + 'license_key' => $this->license_key, + 'site_url' => home_url(), + 'site_name' => get_bloginfo('name'), + 'wp_version' => get_bloginfo('version'), + 'php_version' => PHP_VERSION + ), + 'headers' => array( + 'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url() + ) + )); + + if (is_wp_error($response)) { + return array( + 'success' => false, + 'message' => $response->get_error_message() + ); + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (!$data) { + return array( + 'success' => false, + 'message' => __('Invalid response from server.') + ); + } + + return $data; + } + + /** + * Deactivate license + */ + public function deactivate_license() { + if (empty($this->license_key)) { + return array( + 'success' => false, + 'message' => __('No license key to deactivate.') + ); + } + + $url = $this->update_server . 'wp-json/wpdd/v1/deactivate-license'; + + $response = wp_remote_post($url, array( + 'timeout' => 15, + 'body' => array( + 'license_key' => $this->license_key, + 'site_url' => home_url() + ), + 'headers' => array( + 'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url() + ) + )); + + if (is_wp_error($response)) { + return array( + 'success' => false, + 'message' => $response->get_error_message() + ); + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + return $data ?: array( + 'success' => false, + 'message' => __('Invalid response from server.') + ); + } + + /** + * Add a simple settings page for license management + */ + private function add_settings_page() { + add_action('admin_menu', array($this, 'add_license_menu')); + add_action('admin_init', array($this, 'handle_license_actions')); + } + + /** + * Add license menu page + */ + public function add_license_menu() { + add_options_page( + sprintf(__('%s License', 'default'), $this->plugin_slug), + sprintf(__('%s License', 'default'), $this->plugin_slug), + 'manage_options', + $this->plugin_slug . '-license', + array($this, 'render_license_page') + ); + } + + /** + * Handle license activation/deactivation + */ + public function handle_license_actions() { + if (!current_user_can('manage_options')) { + return; + } + + $option_key = $this->plugin_slug . '_license_key'; + + if (isset($_POST['activate_license'])) { + if (!wp_verify_nonce($_POST['license_nonce'], 'wpdd_license_nonce')) { + return; + } + + $license_key = sanitize_text_field($_POST['license_key']); + $result = $this->activate_license($license_key); + + if ($result['success']) { + update_option($option_key, $license_key); + $this->license_key = $license_key; + add_settings_error('wpdd_license', 'activated', $result['message'], 'updated'); + } else { + add_settings_error('wpdd_license', 'activation_failed', $result['message'], 'error'); + } + } + + if (isset($_POST['deactivate_license'])) { + if (!wp_verify_nonce($_POST['license_nonce'], 'wpdd_license_nonce')) { + return; + } + + $result = $this->deactivate_license(); + + if ($result['success']) { + delete_option($option_key); + $this->license_key = ''; + add_settings_error('wpdd_license', 'deactivated', $result['message'], 'updated'); + } else { + add_settings_error('wpdd_license', 'deactivation_failed', $result['message'], 'error'); + } + } + } + + /** + * Render license management page + */ + public function render_license_page() { + $option_key = $this->plugin_slug . '_license_key'; + $license_key = get_option($option_key, ''); + $license_status = $this->validate_license(); + + ?> +
+

plugin_slug)); ?>

+ + + +
+ + + + + + + +
+ + + + + + + + + + + +
+ + +

+ +

+ +

+ +

+ +
+ +

+
    +
  1. +
  2. +
  3. +
+
+ transient_key); + delete_site_transient('update_plugins'); + } +} + +} // End class exists check \ No newline at end of file diff --git a/wp-digital-download.php b/wp-digital-download.php index 6ef98e2..2108d57 100644 --- a/wp-digital-download.php +++ b/wp-digital-download.php @@ -39,6 +39,7 @@ final class WP_Digital_Download { private function load_dependencies() { $files = array( 'includes/class-wpdd-install.php', + 'includes/class-wpdd-setup.php', 'includes/class-wpdd-post-types.php', 'includes/class-wpdd-roles.php', 'includes/class-wpdd-metaboxes.php', @@ -46,6 +47,8 @@ final class WP_Digital_Download { 'includes/class-wpdd-paypal.php', 'includes/class-wpdd-download-handler.php', 'includes/class-wpdd-customer.php', + 'includes/class-wpdd-license-manager.php', + 'includes/class-wpdd-api.php', 'includes/class-wpdd-orders.php', 'includes/class-wpdd-file-protection.php', 'includes/class-wpdd-watermark.php', @@ -168,7 +171,25 @@ final class WP_Digital_Download { error_log('WPDD Error: WPDD_Ajax class not found'); } + if (class_exists('WPDD_License_Manager')) { + WPDD_License_Manager::init(); + } else { + error_log('WPDD Error: WPDD_License_Manager class not found'); + } + + if (class_exists('WPDD_API')) { + WPDD_API::init(); + } else { + error_log('WPDD Error: WPDD_API class not found'); + } + if (is_admin()) { + if (class_exists('WPDD_Setup')) { + WPDD_Setup::init(); + } else { + error_log('WPDD Error: WPDD_Setup class not found'); + } + if (class_exists('WPDD_Admin')) { WPDD_Admin::init(); } else {