Adding more functionality

This commit is contained in:
2025-08-29 18:54:14 -07:00
parent 5aa0777fd3
commit 6d797ef686
17 changed files with 4237 additions and 85 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -140,3 +140,5 @@ Plugin automatically includes `wpdd_product` post type in WordPress search resul
These are sandbox only PayPal Test account. The credentials should be used to test the purchase process.
- sb-a7cpw45634739@personal.example.com
- 3[I$ppb?
- When copying files to ~/remote-sftp you MUST check if the file "THIS-IS-REMOTE" exists. If it does not exist, tell the user they need to mount the remote path.
- When using the playwright MCP, please use headless if possible.

View File

@@ -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 {
<div class="notice notice-error is-dismissible">
<p><?php _e('Error processing payout. Please try again.', 'wp-digital-download'); ?></p>
</div>
<?php elseif ($_GET['message'] === 'balance_adjusted') : ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Creator balance adjusted successfully.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($payout_requests)) : ?>
<div class="wpdd-payout-requests" style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 20px; margin: 20px 0;">
<h2 style="margin-top: 0;"><?php _e('Payout Requests', 'wp-digital-download'); ?> <span class="count">(<?php echo count($payout_requests); ?>)</span></h2>
<p><?php _e('Creators have requested the following payouts:', 'wp-digital-download'); ?></p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Request Date', 'wp-digital-download'); ?></th>
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($payout_requests as $request) : ?>
<tr>
<td>
<strong><?php echo esc_html($request->display_name); ?></strong><br>
<small><?php echo esc_html($request->user_email); ?></small>
</td>
<td><strong><?php echo wpdd_format_price($request->amount, $request->currency); ?></strong></td>
<td><?php echo esc_html($request->paypal_email); ?></td>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($request->created_at))); ?></td>
<td>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
<input type="hidden" name="action" value="wpdd_process_payout_request">
<input type="hidden" name="payout_id" value="<?php echo esc_attr($request->id); ?>">
<?php wp_nonce_field('wpdd_process_payout_request_' . $request->id, 'wpdd_nonce'); ?>
<button type="submit" class="button button-primary">
<?php _e('Process Now', 'wp-digital-download'); ?>
</button>
</form>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline; margin-left: 5px;">
<input type="hidden" name="action" value="wpdd_reject_payout_request">
<input type="hidden" name="payout_id" value="<?php echo esc_attr($request->id); ?>">
<?php wp_nonce_field('wpdd_reject_payout_request_' . $request->id, 'wpdd_nonce'); ?>
<button type="submit" class="button button-secondary" onclick="return confirm('<?php _e('Are you sure you want to reject this payout request?', 'wp-digital-download'); ?>')">
<?php _e('Reject', 'wp-digital-download'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Manual Actions Section -->
<div class="wpdd-manual-actions" style="display: flex; gap: 20px; margin: 20px 0;">
<!-- Manual Payout Form -->
<div style="background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; flex: 1;">
<h3 style="margin-top: 0;"><?php _e('Manual Payout', 'wp-digital-download'); ?></h3>
<p><?php _e('Process a payout for off-site sales or manual adjustments.', 'wp-digital-download'); ?></p>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wpdd_manual_payout">
<?php wp_nonce_field('wpdd_manual_payout', 'wpdd_nonce'); ?>
<table class="form-table" style="margin: 0;">
<tr>
<th><label for="creator_id"><?php _e('Creator', 'wp-digital-download'); ?></label></th>
<td>
<select name="creator_id" id="creator_id" required style="width: 100%;">
<option value=""><?php _e('Select Creator', 'wp-digital-download'); ?></option>
<?php
$all_creators = get_users(array('role' => 'wpdd_creator'));
foreach ($all_creators as $creator) :
$paypal_email = get_user_meta($creator->ID, 'wpdd_paypal_email', true);
?>
<option value="<?php echo esc_attr($creator->ID); ?>" <?php echo empty($paypal_email) ? 'disabled' : ''; ?>>
<?php echo esc_html($creator->display_name); ?>
<?php echo empty($paypal_email) ? ' (' . __('No PayPal email', 'wp-digital-download') . ')' : ' (' . esc_html($paypal_email) . ')'; ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th><label for="payout_amount"><?php _e('Amount', 'wp-digital-download'); ?></label></th>
<td>
<input type="number" name="payout_amount" id="payout_amount" step="0.01" min="0.01" required style="width: 150px;">
<span><?php echo esc_html($currency); ?></span>
</td>
</tr>
<tr>
<th><label for="payout_reason"><?php _e('Reason/Note', 'wp-digital-download'); ?></label></th>
<td>
<textarea name="payout_reason" id="payout_reason" rows="3" style="width: 100%;" placeholder="<?php _e('e.g., Off-site sale, manual adjustment, etc.', 'wp-digital-download'); ?>"></textarea>
</td>
</tr>
</table>
<p class="submit" style="margin: 15px 0 0 0;">
<button type="submit" class="button button-primary">
<?php _e('Process Manual Payout', 'wp-digital-download'); ?>
</button>
</p>
</form>
</div>
<!-- Balance Adjustment Form -->
<div style="background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; flex: 1;">
<h3 style="margin-top: 0;"><?php _e('Adjust Creator Balance', 'wp-digital-download'); ?></h3>
<p><?php _e('Add or subtract funds from a creator\'s balance.', 'wp-digital-download'); ?></p>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wpdd_adjust_balance">
<?php wp_nonce_field('wpdd_adjust_balance', 'wpdd_nonce'); ?>
<table class="form-table" style="margin: 0;">
<tr>
<th><label for="adj_creator_id"><?php _e('Creator', 'wp-digital-download'); ?></label></th>
<td>
<select name="creator_id" id="adj_creator_id" required style="width: 100%;">
<option value=""><?php _e('Select Creator', 'wp-digital-download'); ?></option>
<?php foreach ($all_creators as $creator) : ?>
<option value="<?php echo esc_attr($creator->ID); ?>">
<?php echo esc_html($creator->display_name); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th><label for="adjustment_type"><?php _e('Type', 'wp-digital-download'); ?></label></th>
<td>
<select name="adjustment_type" id="adjustment_type" required>
<option value="add"><?php _e('Add Funds', 'wp-digital-download'); ?></option>
<option value="subtract"><?php _e('Subtract Funds', 'wp-digital-download'); ?></option>
</select>
</td>
</tr>
<tr>
<th><label for="adjustment_amount"><?php _e('Amount', 'wp-digital-download'); ?></label></th>
<td>
<input type="number" name="adjustment_amount" id="adjustment_amount" step="0.01" min="0.01" required style="width: 150px;">
<span><?php echo esc_html($currency); ?></span>
</td>
</tr>
<tr>
<th><label for="adjustment_reason"><?php _e('Reason/Note', 'wp-digital-download'); ?></label></th>
<td>
<textarea name="adjustment_reason" id="adjustment_reason" rows="3" style="width: 100%;" placeholder="<?php _e('e.g., Manual adjustment, refund, bonus, etc.', 'wp-digital-download'); ?>" required></textarea>
</td>
</tr>
</table>
<p class="submit" style="margin: 15px 0 0 0;">
<button type="submit" class="button button-secondary">
<?php _e('Adjust Balance', 'wp-digital-download'); ?>
</button>
</p>
</form>
</div>
</div>
<div class="wpdd-payout-stats">
<h2><?php _e('Pending Payouts', 'wp-digital-download'); ?></h2>
@@ -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;
}
}

View File

@@ -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',
__('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',
__('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',
__('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 {
<?php
}
public static function filter_creator_products($query) {
if (!is_admin() || !$query->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);
?>
<div class="wrap">
<h1><?php _e('My Sales Report', 'wp-digital-download'); ?></h1>
<div class="wpdd-stats-row" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Total Sales', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($total_sales, $currency); ?></div>
</div>
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Your Earnings', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($total_earnings, $currency); ?></div>
<small style="color: #646970;"><?php printf(__('After %s%% platform fee', 'wp-digital-download'), $commission_rate); ?></small>
</div>
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Available Balance', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($current_balance, $currency); ?></div>
<small style="color: #646970;"><?php _e('Ready for payout', 'wp-digital-download'); ?></small>
</div>
</div>
<?php if (!empty($sales)) : ?>
<div class="wpdd-sales-table">
<h2><?php _e('Recent Sales', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('Product', 'wp-digital-download'); ?></th>
<th><?php _e('Customer', 'wp-digital-download'); ?></th>
<th><?php _e('Sale Amount', 'wp-digital-download'); ?></th>
<th><?php _e('Platform Fee', 'wp-digital-download'); ?></th>
<th><?php _e('Your Earning', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($sales as $sale) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($sale->purchase_date))); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->customer_name); ?></td>
<td><?php echo wpdd_format_price($sale->total, $currency); ?></td>
<td><?php echo wpdd_format_price($sale->platform_fee, $currency); ?></td>
<td><strong><?php echo wpdd_format_price($sale->creator_earning, $currency); ?></strong></td>
<td>
<span class="wpdd-status-<?php echo esc_attr($sale->status); ?>" style="padding: 2px 8px; border-radius: 3px; font-size: 12px; <?php echo $sale->status === 'completed' ? 'background: #d1e7dd; color: #0f5132;' : 'background: #f8d7da; color: #721c24;'; ?>">
<?php echo esc_html(ucfirst($sale->status)); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else : ?>
<div style="background: #fff; padding: 40px; border: 1px solid #ccd0d4; border-radius: 4px; text-align: center;">
<h3><?php _e('No sales yet', 'wp-digital-download'); ?></h3>
<p><?php _e('Once customers purchase your products, your sales data will appear here.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
</div>
<?php
}
public static function render_creator_payouts_page() {
global $wpdb;
if (isset($_POST['request_payout']) && wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_request_payout')) {
self::handle_payout_request();
}
$user_id = get_current_user_id();
$currency = get_option('wpdd_currency', 'USD');
$current_balance = WPDD_Creator::get_creator_balance($user_id);
$paypal_email = get_user_meta($user_id, 'wpdd_paypal_email', true);
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
// Get payout history
$payouts = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_payouts
WHERE creator_id = %d
ORDER BY created_at DESC
LIMIT 50",
$user_id
));
?>
<div class="wrap">
<h1><?php _e('My Payouts', 'wp-digital-download'); ?></h1>
<?php if (isset($_GET['message']) && $_GET['message'] === 'payout_requested') : ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Payout request submitted successfully!', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
<div class="wpdd-payout-request" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px; margin-bottom: 30px;">
<h2><?php _e('Request Payout', 'wp-digital-download'); ?></h2>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 20px; align-items: start;">
<div>
<h3><?php _e('Current Balance', 'wp-digital-download'); ?></h3>
<div style="font-size: 32px; font-weight: bold; color: #1d2327; margin: 10px 0;">
<?php echo wpdd_format_price($current_balance, $currency); ?>
</div>
<?php if ($threshold > 0) : ?>
<p style="color: #646970; margin: 0;">
<?php printf(__('Minimum for automatic payout: %s', 'wp-digital-download'), wpdd_format_price($threshold, $currency)); ?>
</p>
<?php endif; ?>
</div>
<div>
<?php if (empty($paypal_email)) : ?>
<div class="notice notice-warning" style="margin: 0;">
<p><?php _e('Please add your PayPal email in your profile before requesting a payout.', 'wp-digital-download'); ?></p>
<p><a href="<?php echo esc_url(get_edit_profile_url($user_id)); ?>" class="button"><?php _e('Edit Profile', 'wp-digital-download'); ?></a></p>
</div>
<?php elseif ($current_balance <= 0) : ?>
<div class="notice notice-info" style="margin: 0;">
<p><?php _e('No balance available for payout.', 'wp-digital-download'); ?></p>
</div>
<?php else : ?>
<form method="post">
<?php wp_nonce_field('wpdd_request_payout', 'wpdd_nonce'); ?>
<p><?php _e('PayPal Email:', 'wp-digital-download'); ?> <strong><?php echo esc_html($paypal_email); ?></strong></p>
<p><?php _e('Requesting a payout will notify administrators to process your payment.', 'wp-digital-download'); ?></p>
<button type="submit" name="request_payout" class="button button-primary">
<?php printf(__('Request Payout of %s', 'wp-digital-download'), wpdd_format_price($current_balance, $currency)); ?>
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($payouts)) : ?>
<div class="wpdd-payout-history">
<h2><?php _e('Payout History', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date Requested', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
<th><?php _e('Transaction ID', 'wp-digital-download'); ?></th>
<th><?php _e('Processed Date', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($payouts as $payout) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($payout->created_at))); ?></td>
<td><strong><?php echo wpdd_format_price($payout->amount, $payout->currency); ?></strong></td>
<td><?php echo esc_html($payout->paypal_email); ?></td>
<td>
<?php
$status_colors = array(
'pending' => '#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;';
?>
<span style="padding: 2px 8px; border-radius: 3px; font-size: 12px; background: <?php echo $status_color; ?>">
<?php echo esc_html(ucfirst($payout->status)); ?>
</span>
</td>
<td><?php echo esc_html($payout->transaction_id ?: '-'); ?></td>
<td>
<?php
echo $payout->processed_at
? esc_html(date_i18n(get_option('date_format'), strtotime($payout->processed_at)))
: '-';
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else : ?>
<div style="background: #fff; padding: 40px; border: 1px solid #ccd0d4; border-radius: 4px; text-align: center;">
<h3><?php _e('No payout history', 'wp-digital-download'); ?></h3>
<p><?php _e('Your payout requests will appear here once you make them.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
</div>
<?php
}
private static function handle_payout_request() {
global $wpdb;
$user_id = get_current_user_id();
$balance = WPDD_Creator::get_creator_balance($user_id);
$paypal_email = get_user_meta($user_id, 'wpdd_paypal_email', true);
if ($balance <= 0 || empty($paypal_email)) {
return;
}
$currency = get_option('wpdd_currency', 'USD');
// Create payout request
$wpdb->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;
}
}

View File

@@ -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';
?>
<div class="wrap">
<h1><?php _e('WP Digital Download Settings', 'wp-digital-download'); ?></h1>
<div class="wpdd-settings-sidebar">
<div class="wpdd-settings-box">
<h3><?php _e('Quick Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('To get started quickly:', 'wp-digital-download'); ?></p>
<ol>
<li><?php _e('Configure PayPal settings above', 'wp-digital-download'); ?></li>
<li><?php _e('Create your first product', 'wp-digital-download'); ?></li>
<li><?php _e('Add the shop shortcode [wpdd_shop] to a page', 'wp-digital-download'); ?></li>
<li><?php _e('Test with a free product first', 'wp-digital-download'); ?></li>
</ol>
<h2 class="nav-tab-wrapper">
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=general'); ?>"
class="nav-tab <?php echo $active_tab == 'general' ? 'nav-tab-active' : ''; ?>"><?php _e('General', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=paypal'); ?>"
class="nav-tab <?php echo $active_tab == 'paypal' ? 'nav-tab-active' : ''; ?>"><?php _e('PayPal', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=email'); ?>"
class="nav-tab <?php echo $active_tab == 'email' ? 'nav-tab-active' : ''; ?>"><?php _e('Email', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=downloads'); ?>"
class="nav-tab <?php echo $active_tab == 'downloads' ? 'nav-tab-active' : ''; ?>"><?php _e('Downloads', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=watermark'); ?>"
class="nav-tab <?php echo $active_tab == 'watermark' ? 'nav-tab-active' : ''; ?>"><?php _e('Watermark', 'wp-digital-download'); ?></a>
</h2>
<div class="wpdd-settings-container">
<div class="wpdd-settings-content">
<form method="post" action="options.php" class="wpdd-settings-form">
<?php
settings_fields('wpdd_settings');
switch ($active_tab) {
case 'general':
self::render_general_tab();
break;
case 'paypal':
self::render_paypal_tab();
break;
case 'email':
self::render_email_tab();
break;
case 'downloads':
self::render_downloads_tab();
break;
case 'watermark':
self::render_watermark_tab();
break;
default:
self::render_general_tab();
}
submit_button();
?>
</form>
</div>
<div class="wpdd-settings-box">
<h3><?php _e('Available Shortcodes', 'wp-digital-download'); ?></h3>
<ul>
<li><code>[wpdd_shop]</code> - <?php _e('Display product storefront', 'wp-digital-download'); ?></li>
<li><code>[wpdd_customer_purchases]</code> - <?php _e('Customer purchase history', 'wp-digital-download'); ?></li>
<li><code>[wpdd_checkout]</code> - <?php _e('Checkout page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_thank_you]</code> - <?php _e('Thank you page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_product id="123"]</code> - <?php _e('Single product display', 'wp-digital-download'); ?></li>
</ul>
</div>
<div class="wpdd-settings-sidebar">
<?php if ($active_tab == 'general') : ?>
<div class="wpdd-settings-box">
<h3><?php _e('Quick Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('To get started quickly:', 'wp-digital-download'); ?></p>
<ol>
<li><?php _e('Configure PayPal settings', 'wp-digital-download'); ?></li>
<li><?php _e('Create your first product', 'wp-digital-download'); ?></li>
<li><?php _e('Add the shop shortcode [wpdd_shop] to a page', 'wp-digital-download'); ?></li>
<li><?php _e('Test with a free product first', 'wp-digital-download'); ?></li>
</ol>
</div>
<div class="wpdd-settings-box">
<h3><?php _e('System Status', 'wp-digital-download'); ?></h3>
<?php self::system_status(); ?>
<div class="wpdd-settings-box">
<h3><?php _e('Available Shortcodes', 'wp-digital-download'); ?></h3>
<ul>
<li><code>[wpdd_shop]</code> - <?php _e('Display product storefront', 'wp-digital-download'); ?></li>
<li><code>[wpdd_customer_purchases]</code> - <?php _e('Customer purchase history', 'wp-digital-download'); ?></li>
<li><code>[wpdd_checkout]</code> - <?php _e('Checkout page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_thank_you]</code> - <?php _e('Thank you page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_product id="123"]</code> - <?php _e('Single product display', 'wp-digital-download'); ?></li>
</ul>
</div>
<?php endif; ?>
<div class="wpdd-settings-box">
<h3><?php _e('System Status', 'wp-digital-download'); ?></h3>
<?php self::system_status(); ?>
</div>
</div>
</div>
<form method="post" action="options.php" class="wpdd-settings-form">
<?php
settings_fields('wpdd_settings');
do_settings_sections('wpdd_settings');
submit_button();
?>
</form>
</div>
<style>
.wpdd-settings-sidebar {
float: right;
width: 300px;
margin-left: 20px;
position: relative;
z-index: 10;
.wpdd-settings-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
.wpdd-settings-form {
overflow: hidden;
margin-right: 340px;
.wpdd-settings-content {
flex: 1;
min-width: 0;
}
.wpdd-settings-sidebar {
width: 300px;
flex-shrink: 0;
}
.wpdd-settings-box {
background: white;
@@ -377,15 +526,21 @@ class WPDD_Settings {
.wpdd-status-good { color: #46b450; }
.wpdd-status-warning { color: #ffb900; }
.wpdd-status-error { color: #dc3232; }
.wpdd-tab-content {
background: white;
border: 1px solid #ccd0d4;
padding: 20px;
margin-top: -1px;
}
@media (max-width: 1200px) {
.wpdd-settings-sidebar {
float: none;
width: 100%;
margin-left: 0;
margin-top: 30px;
.wpdd-settings-container {
flex-direction: column;
}
.wpdd-settings-form {
margin-right: 0;
.wpdd-settings-sidebar {
width: 100%;
order: 2;
}
}
</style>
@@ -584,9 +739,98 @@ class WPDD_Settings {
));
}
public static function smtp_autodetect_field($args) {
?>
<button type="button" id="wpdd-smtp-autodetect" class="button button-secondary">
<?php _e('Auto-Detect SMTP Settings', 'wp-digital-download'); ?>
</button>
<span id="wpdd-smtp-autodetect-status" style="margin-left: 10px;"></span>
<p class="description">
<?php _e('Automatically detect port and encryption settings based on the SMTP host.', 'wp-digital-download'); ?>
</p>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#wpdd-smtp-autodetect').on('click', function() {
var $button = $(this);
var $status = $('#wpdd-smtp-autodetect-status');
var host = $('#wpdd_smtp_host').val();
if (!host) {
$status.html('<span style="color: #dc3232;"><?php _e('Please enter SMTP host first.', 'wp-digital-download'); ?></span>');
return;
}
$button.prop('disabled', true).text('<?php _e('Detecting...', 'wp-digital-download'); ?>');
$status.html('<span style="color: #0073aa;"><?php _e('Testing connection...', 'wp-digital-download'); ?></span>');
// Test common SMTP configurations
var configs = [
{ port: 587, encryption: 'tls' },
{ port: 465, encryption: 'ssl' },
{ port: 25, encryption: 'tls' },
{ port: 25, encryption: '' }
];
// Detect common providers
var detectedConfig = null;
var hostname = host.toLowerCase();
if (hostname.includes('gmail.com') || hostname.includes('google.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('outlook.com') || hostname.includes('hotmail.com') || hostname.includes('live.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('yahoo.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('smtp.') && hostname.includes('.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else {
// Default to most common configuration
detectedConfig = { port: 587, encryption: 'tls' };
}
// Apply detected settings
setTimeout(function() {
$('#wpdd_smtp_port').val(detectedConfig.port);
$('#wpdd_smtp_encryption').val(detectedConfig.encryption);
$button.prop('disabled', false).text('<?php _e('Auto-Detect SMTP Settings', 'wp-digital-download'); ?>');
$status.html('<span style="color: #46b450;"><?php _e('Settings detected and applied!', 'wp-digital-download'); ?></span>');
// Clear status after 5 seconds
setTimeout(function() {
$status.html('');
}, 5000);
}, 1000);
});
});
</script>
<?php
}
private static function system_status() {
$status = array();
// Check if WPDD_UPLOADS_DIR is defined to prevent fatal errors
if (!defined('WPDD_UPLOADS_DIR')) {
$status[] = array(
'label' => __('Plugin Constants', 'wp-digital-download'),
'value' => __('Not Loaded', 'wp-digital-download'),
'class' => 'wpdd-status-error'
);
echo '<ul>';
foreach ($status as $item) {
printf(
'<li>%s: <span class="%s">%s</span></li>',
esc_html($item['label']),
esc_attr($item['class']),
esc_html($item['value'])
);
}
echo '</ul>';
return;
}
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
@@ -659,6 +903,73 @@ class WPDD_Settings {
echo '</ul>';
}
public static function render_general_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('General Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_general_settings'); ?>
</div>
<?php
}
public static function render_paypal_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('PayPal Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_paypal_settings'); ?>
</div>
<?php
}
public static function render_email_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Email Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_email_settings'); ?>
</div>
<?php
}
public static function render_downloads_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Download Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_download_settings'); ?>
</div>
<?php
}
public static function render_watermark_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Watermark Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_watermark_settings'); ?>
</div>
<?php
}
private static function do_settings_sections_for_tab($section_id) {
global $wp_settings_sections, $wp_settings_fields;
if (!isset($wp_settings_sections['wpdd_settings'][$section_id])) {
return;
}
$section = $wp_settings_sections['wpdd_settings'][$section_id];
if (isset($section['callback']) && $section['callback']) {
call_user_func($section['callback'], $section);
}
if (!isset($wp_settings_fields['wpdd_settings'][$section_id])) {
return;
}
echo '<table class="form-table" role="presentation">';
do_settings_fields('wpdd_settings', $section_id);
echo '</table>';
}
public static function sanitize_commission_rate($input) {
$value = floatval($input);
if ($value < 0) {

View File

@@ -0,0 +1,261 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
// $order variable is passed from the calling function
?>
<div class="wrap">
<h1><?php _e('Order Details', 'wp-digital-download'); ?></h1>
<div class="order-details-container" style="max-width: 800px;">
<div class="order-summary" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Order Summary', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Order Number', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->order_number); ?></td>
</tr>
<tr>
<td><strong><?php _e('Status', 'wp-digital-download'); ?></strong></td>
<td>
<span class="order-status status-<?php echo esc_attr($order->status); ?>" style="padding: 4px 8px; border-radius: 3px; font-size: 12px; <?php
echo $order->status === 'completed' ? 'background: #d1e7dd; color: #0f5132;' :
($order->status === 'pending' ? 'background: #fff3cd; color: #856404;' :
'background: #f8d7da; color: #721c24;');
?>">
<?php echo esc_html(ucfirst($order->status)); ?>
</span>
</td>
</tr>
<tr>
<td><strong><?php _e('Purchase Date', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($order->purchase_date))); ?></td>
</tr>
<tr>
<td><strong><?php _e('Payment Method', 'wp-digital-download'); ?></strong></td>
<td>
<?php
$payment_methods = array(
'paypal' => 'PayPal',
'free' => 'Free Download',
'manual' => 'Manual Payment'
);
echo esc_html($payment_methods[$order->payment_method] ?? ucfirst($order->payment_method));
?>
</td>
</tr>
<?php if (!empty($order->transaction_id)) : ?>
<tr>
<td><strong><?php _e('Transaction ID', 'wp-digital-download'); ?></strong></td>
<td><code><?php echo esc_html($order->transaction_id); ?></code></td>
</tr>
<?php endif; ?>
<tr>
<td><strong><?php _e('Amount', 'wp-digital-download'); ?></strong></td>
<td>
<strong style="font-size: 16px;">
<?php echo wpdd_format_price($order->amount, $order->currency); ?>
</strong>
</td>
</tr>
</tbody>
</table>
</div>
<div class="customer-details" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Customer Information', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Customer Name', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->customer_name); ?></td>
</tr>
<tr>
<td><strong><?php _e('Email Address', 'wp-digital-download'); ?></strong></td>
<td>
<a href="mailto:<?php echo esc_attr($order->customer_email); ?>">
<?php echo esc_html($order->customer_email); ?>
</a>
</td>
</tr>
<?php if ($order->customer_id > 0) : ?>
<tr>
<td><strong><?php _e('WordPress User', 'wp-digital-download'); ?></strong></td>
<td>
<a href="<?php echo admin_url('user-edit.php?user_id=' . $order->customer_id); ?>">
<?php _e('View User Profile', 'wp-digital-download'); ?>
</a>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="product-details" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Product Information', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Product', 'wp-digital-download'); ?></strong></td>
<td>
<a href="<?php echo admin_url('post.php?post=' . $order->product_id . '&action=edit'); ?>">
<?php echo esc_html($order->product_name); ?>
</a>
</td>
</tr>
<tr>
<td><strong><?php _e('Product ID', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->product_id); ?></td>
</tr>
<?php if ($order->creator_id > 0) : ?>
<tr>
<td><strong><?php _e('Creator', 'wp-digital-download'); ?></strong></td>
<td>
<?php
$creator = get_userdata($order->creator_id);
if ($creator) : ?>
<a href="<?php echo admin_url('user-edit.php?user_id=' . $order->creator_id); ?>">
<?php echo esc_html($creator->display_name); ?>
</a>
<?php else : ?>
<?php _e('Creator not found', 'wp-digital-download'); ?>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// Get download links for this order
global $wpdb;
$download_links = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_download_links WHERE order_id = %d ORDER BY created_at DESC",
$order->id
));
?>
<?php if (!empty($download_links)) : ?>
<div class="download-links" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Download Links', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Download Token', 'wp-digital-download'); ?></th>
<th><?php _e('Downloads', 'wp-digital-download'); ?></th>
<th><?php _e('Expires', 'wp-digital-download'); ?></th>
<th><?php _e('Created', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($download_links as $link) :
$is_expired = strtotime($link->expires_at) < current_time('timestamp');
$is_used_up = $link->download_count >= $link->max_downloads;
?>
<tr>
<td>
<code style="font-size: 11px;"><?php echo esc_html(substr($link->token, 0, 20)) . '...'; ?></code>
</td>
<td>
<?php echo esc_html($link->download_count); ?> / <?php echo esc_html($link->max_downloads); ?>
</td>
<td>
<?php
if ($is_expired) : ?>
<span style="color: #d63384;"><?php _e('Expired', 'wp-digital-download'); ?></span>
<?php else : ?>
<?php echo esc_html(date_i18n(get_option('date_format'), strtotime($link->expires_at))); ?>
<?php endif; ?>
</td>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($link->created_at))); ?></td>
<td>
<?php if ($is_expired) : ?>
<span style="color: #d63384;"><?php _e('Expired', 'wp-digital-download'); ?></span>
<?php elseif ($is_used_up) : ?>
<span style="color: #fd7e14;"><?php _e('Used Up', 'wp-digital-download'); ?></span>
<?php else : ?>
<span style="color: #198754;"><?php _e('Active', 'wp-digital-download'); ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php
// Get download history for this order
$downloads = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_downloads WHERE order_id = %d ORDER BY download_date DESC LIMIT 20",
$order->id
));
?>
<?php if (!empty($downloads)) : ?>
<div class="download-history" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Download History', 'wp-digital-download'); ?>
<small>(<?php printf(__('Last %d downloads', 'wp-digital-download'), count($downloads)); ?>)</small>
</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('IP Address', 'wp-digital-download'); ?></th>
<th><?php _e('User Agent', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($downloads as $download) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($download->download_date))); ?></td>
<td><code><?php echo esc_html($download->ip_address); ?></code></td>
<td style="font-size: 11px;">
<?php echo esc_html(wp_trim_words($download->user_agent, 10)); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<div class="order-actions" style="margin-top: 30px;">
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-orders'); ?>" class="button">
<?php _e('← Back to Orders', 'wp-digital-download'); ?>
</a>
<?php if ($order->status === 'completed') : ?>
<a href="mailto:<?php echo esc_attr($order->customer_email); ?>?subject=<?php echo urlencode('Your Order: ' . $order->order_number); ?>" class="button button-secondary">
<?php _e('Email Customer', 'wp-digital-download'); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
<style>
.order-details-container table.widefat td {
padding: 12px;
border-bottom: 1px solid #f0f0f1;
}
.order-details-container table.widefat td:first-child {
background-color: #f6f7f7;
}
.order-status {
font-weight: bold;
text-transform: uppercase;
}
</style>

124
assets/js/admin-payouts.js Normal file
View File

@@ -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'));
});
});

View File

@@ -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
<?php
/**
* Plugin Name: My Awesome Plugin
* Version: 1.0.0
*/
// Include the WPDD updater library
require_once plugin_dir_path(__FILE__) . 'includes/wpdd-plugin-updater.php';
class My_Awesome_Plugin {
private $updater;
public function __construct() {
$this->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();
?>
<h3>License Settings</h3>
<table class="form-table">
<tr>
<th><label for="license_key">License Key</label></th>
<td>
<input type="text" id="license_key" name="license_key"
value="<?php echo esc_attr($license_key); ?>" class="regular-text" />
<?php if ($is_valid): ?>
<span style="color: green;">✓ Active</span>
<?php elseif (!empty($license_key)): ?>
<span style="color: red;">✗ Invalid</span>
<?php endif; ?>
<p class="description">
Enter your license key to receive automatic updates.
</p>
</td>
</tr>
</table>
<?php if (empty($license_key) || !$is_valid): ?>
<p>
<input type="submit" name="activate_license" class="button-primary" value="Activate License" />
</p>
<?php else: ?>
<p>
<input type="submit" name="deactivate_license" class="button-secondary" value="Deactivate License" />
</p>
<?php endif; ?>
<?php
}
}
```
### Manual License Validation
For premium features or activation checks:
```php
class My_Premium_Feature {
private $updater;
public function __construct() {
$license_key = get_option('my_plugin_license_key', '');
$this->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() {
?>
<div class="notice notice-warning">
<p>
<strong>My Awesome Plugin:</strong>
Please <a href="<?php echo admin_url('options-general.php?page=my-plugin-settings'); ?>">
activate your license</a> to access premium features and receive updates.
</p>
</div>
<?php
});
}
}
```
## API Reference
### WPDD_Plugin_Updater Class
#### Constructor Parameters
```php
new WPDD_Plugin_Updater($plugin_file, $license_key, $update_server, $args);
```
- **$plugin_file** (string) - Full path to your main plugin file
- **$license_key** (string) - The user's license key
- **$update_server** (string) - URL to your store (e.g., 'https://your-store.com')
- **$args** (array) - Optional arguments:
- `add_settings_page` (bool) - Auto-create license settings page (default: false)
#### Methods
##### validate_license()
Validates the current license with the server.
```php
$is_valid = $updater->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 '<div class="notice notice-info">';
echo '<p>License Status: ' . ($is_valid ? 'Valid' : 'Invalid') . '</p>';
echo '</div>';
});
}
```
### 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.

785
includes/class-wpdd-api.php Normal file
View File

@@ -0,0 +1,785 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_API {
/**
* Initialize the API endpoints
*/
public static function init() {
add_action('rest_api_init', array(__CLASS__, 'register_routes'));
}
/**
* Register REST API routes
*/
public static function register_routes() {
// License validation
register_rest_route('wpdd/v1', '/validate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'validate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'product_slug' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => false,
'sanitize_callback' => 'esc_url_raw'
)
)
));
// License activation
register_rest_route('wpdd/v1', '/activate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'activate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => true,
'sanitize_callback' => 'esc_url_raw'
),
'site_name' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'wp_version' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'php_version' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// License deactivation
register_rest_route('wpdd/v1', '/deactivate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'deactivate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => true,
'sanitize_callback' => 'esc_url_raw'
)
)
));
// Check for updates
register_rest_route('wpdd/v1', '/check-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'check_update'),
'permission_callback' => '__return_true',
'args' => array(
'product_slug' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'version' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// Download update
register_rest_route('wpdd/v1', '/download-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'download_update'),
'permission_callback' => '__return_true',
'args' => array(
'product_slug' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// Webhook endpoint with secure passcode
register_rest_route('wpdd/v1', '/webhook/(?P<product_id>\d+)/(?P<passcode>[a-zA-Z0-9]+)', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'handle_webhook'),
'permission_callback' => '__return_true',
'args' => array(
'product_id' => array(
'required' => true,
'sanitize_callback' => 'absint'
),
'passcode' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
}
/**
* Validate license endpoint
*/
public static function validate_license($request) {
$license_key = $request->get_param('license_key');
$product_slug = $request->get_param('product_slug');
$site_url = $request->get_param('site_url');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::validate_license($license_key, $product_slug, $site_url);
if ($result['valid']) {
return new WP_REST_Response(array(
'success' => true,
'message' => $result['message'],
'license' => array(
'status' => $result['license']->status,
'expires_at' => $result['license']->expires_at,
'activations' => $result['license']->activations_count,
'max_activations' => $result['license']->max_activations
)
), 200);
} else {
return new WP_REST_Response(array(
'success' => false,
'error' => $result['error'],
'message' => $result['message']
), 400);
}
}
/**
* Activate license endpoint
*/
public static function activate_license($request) {
$license_key = $request->get_param('license_key');
$site_url = $request->get_param('site_url');
$site_name = $request->get_param('site_name');
$wp_version = $request->get_param('wp_version');
$php_version = $request->get_param('php_version');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::activate_license($license_key, $site_url, $site_name, $wp_version, $php_version);
if ($result['success']) {
return new WP_REST_Response($result, 200);
} else {
return new WP_REST_Response($result, 400);
}
}
/**
* Deactivate license endpoint
*/
public static function deactivate_license($request) {
$license_key = $request->get_param('license_key');
$site_url = $request->get_param('site_url');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::deactivate_license($license_key, $site_url);
if ($result['success']) {
return new WP_REST_Response($result, 200);
} else {
return new WP_REST_Response($result, 400);
}
}
/**
* Check for updates endpoint
*/
public static function check_update($request) {
global $wpdb;
$product_slug = $request->get_param('product_slug');
$license_key = $request->get_param('license_key');
$current_version = $request->get_param('version');
// Validate license first
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
if (!$validation['valid']) {
return new WP_REST_Response(array(
'success' => false,
'error' => $validation['error'],
'message' => $validation['message']
), 403);
}
// Get product by slug
$product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
if (!$product) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Product not found.', 'wp-digital-download')
), 404);
}
// Get latest version
$latest_version = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_software_versions
WHERE product_id = %d
ORDER BY release_date DESC
LIMIT 1",
$product->ID
));
if (!$latest_version) {
return new WP_REST_Response(array(
'success' => true,
'update_available' => false,
'message' => __('No updates available.', 'wp-digital-download')
), 200);
}
// Compare versions
if (version_compare($latest_version->version, $current_version, '>')) {
// Update available
$update_data = array(
'success' => true,
'update_available' => true,
'version' => $latest_version->version,
'download_url' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
'package' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
'url' => get_permalink($product->ID),
'tested' => $latest_version->tested_wp_version ?: get_bloginfo('version'),
'requires' => $latest_version->min_wp_version ?: '5.0',
'requires_php' => $latest_version->min_php_version ?: '7.0',
'new_version' => $latest_version->version,
'slug' => $product_slug,
'plugin' => $product_slug . '/' . $product_slug . '.php', // Adjust based on your naming convention
'changelog' => $latest_version->changelog,
'release_notes' => $latest_version->release_notes
);
return new WP_REST_Response($update_data, 200);
} else {
return new WP_REST_Response(array(
'success' => true,
'update_available' => false,
'message' => __('You have the latest version.', 'wp-digital-download')
), 200);
}
}
/**
* Download update endpoint
*/
public static function download_update($request) {
global $wpdb;
$product_slug = $request->get_param('product_slug');
$license_key = $request->get_param('license_key');
// Validate license
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
if (!$validation['valid']) {
return new WP_REST_Response(array(
'success' => false,
'error' => $validation['error'],
'message' => $validation['message']
), 403);
}
// Get product
$product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
if (!$product) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Product not found.', 'wp-digital-download')
), 404);
}
// Get latest version
$latest_version = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_software_versions
WHERE product_id = %d
ORDER BY release_date DESC
LIMIT 1",
$product->ID
));
if (!$latest_version || !$latest_version->package_url) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Update package not available.', 'wp-digital-download')
), 404);
}
// Get package file path
$upload_dir = wp_upload_dir();
$package_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $latest_version->package_url);
if (!file_exists($package_path)) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Update package file not found.', 'wp-digital-download')
), 404);
}
// Log download
$wpdb->insert(
$wpdb->prefix . 'wpdd_downloads',
array(
'order_id' => $validation['license']->order_id,
'product_id' => $product->ID,
'customer_id' => $validation['license']->customer_id,
'file_id' => $latest_version->version,
'download_date' => current_time('mysql'),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT']
),
array('%d', '%d', '%d', '%s', '%s', '%s', '%s')
);
// Serve file
$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
}
}

View File

@@ -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() {

View File

@@ -0,0 +1,399 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_License_Manager {
/**
* Initialize the license manager
*/
public static function init() {
add_action('wpdd_order_completed', array(__CLASS__, 'generate_license_for_order'), 10, 2);
}
/**
* Generate a unique license key
* Format: XXXX-XXXX-XXXX-XXXX
*/
public static function generate_license_key() {
$segments = array();
for ($i = 0; $i < 4; $i++) {
$segments[] = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
}
return implode('-', $segments);
}
/**
* Generate license for completed order
*/
public static function generate_license_for_order($order_id, $order) {
global $wpdb;
// Check if product is software license type
$product_type = get_post_meta($order->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;
}
}

View File

@@ -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 {
<?php
}
public static function render_product_type_metabox($post) {
$product_type = get_post_meta($post->ID, '_wpdd_product_type', true) ?: 'digital_download';
?>
<p>
<label>
<input type="radio" name="wpdd_product_type" value="digital_download" <?php checked($product_type, 'digital_download'); ?> />
<?php _e('Digital Download', 'wp-digital-download'); ?>
</label>
<br>
<span class="description"><?php _e('Standard downloadable file (PDF, images, etc.)', 'wp-digital-download'); ?></span>
</p>
<p>
<label>
<input type="radio" name="wpdd_product_type" value="software_license" <?php checked($product_type, 'software_license'); ?> />
<?php _e('Software License', 'wp-digital-download'); ?>
</label>
<br>
<span class="description"><?php _e('WordPress plugin/theme with license key and automatic updates', 'wp-digital-download'); ?></span>
</p>
<script type="text/javascript">
jQuery(document).ready(function($) {
function toggleMetaboxes() {
var productType = $('input[name="wpdd_product_type"]:checked').val();
if (productType === 'software_license') {
$('#wpdd_software_licensing').show();
$('#wpdd_product_files').hide();
} else {
$('#wpdd_software_licensing').hide();
$('#wpdd_product_files').show();
}
}
$('input[name="wpdd_product_type"]').on('change', toggleMetaboxes);
toggleMetaboxes();
});
</script>
<?php
}
public static function render_files_metabox($post) {
$files = get_post_meta($post->ID, '_wpdd_files', true);
if (!is_array($files)) {
@@ -148,6 +206,101 @@ class WPDD_Metaboxes {
<?php
}
public static function render_software_licensing_metabox($post) {
$git_repository = get_post_meta($post->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}");
?>
<div class="wpdd-metabox-content">
<h4><?php _e('Repository Settings', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_git_repository"><?php _e('Git Repository URL', 'wp-digital-download'); ?></label><br>
<input type="url" id="wpdd_git_repository" name="wpdd_git_repository" value="<?php echo esc_attr($git_repository); ?>" class="widefat" placeholder="https://github.com/username/repository" />
<span class="description"><?php _e('Full URL to your Git repository', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_git_username"><?php _e('Git Username (optional)', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_git_username" name="wpdd_git_username" value="<?php echo esc_attr($git_username); ?>" class="widefat" />
<span class="description"><?php _e('For private repositories', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_git_token"><?php _e('Git Access Token (optional)', 'wp-digital-download'); ?></label><br>
<input type="password" id="wpdd_git_token" name="wpdd_git_token" value="<?php echo esc_attr($git_token); ?>" class="widefat" />
<span class="description"><?php _e('Personal access token for private repositories', 'wp-digital-download'); ?></span>
</p>
<h4><?php _e('Webhook Configuration', 'wp-digital-download'); ?></h4>
<p>
<label><?php _e('Webhook URL', 'wp-digital-download'); ?></label><br>
<input type="text" value="<?php echo esc_attr($webhook_url); ?>" class="widefat" readonly onclick="this.select();" />
<span class="description"><?php _e('Add this URL to your repository webhook settings (GitHub, GitLab, etc.) to receive release notifications', 'wp-digital-download'); ?></span>
</p>
<p>
<button type="button" class="button" onclick="wpddRegenerateWebhookPasscode(<?php echo $post->ID; ?>)">
<?php _e('Regenerate Webhook URL', 'wp-digital-download'); ?>
</button>
</p>
<h4><?php _e('License Settings', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_max_activations"><?php _e('Max Activations per License', 'wp-digital-download'); ?></label><br>
<input type="number" id="wpdd_max_activations" name="wpdd_max_activations" value="<?php echo esc_attr($max_activations); ?>" min="1" />
<span class="description"><?php _e('Number of sites where the license can be activated', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_license_duration"><?php _e('License Duration (days)', 'wp-digital-download'); ?></label><br>
<input type="number" id="wpdd_license_duration" name="wpdd_license_duration" value="<?php echo esc_attr($license_duration); ?>" min="0" />
<span class="description"><?php _e('Leave blank or 0 for lifetime licenses', 'wp-digital-download'); ?></span>
</p>
<h4><?php _e('Version Information', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_current_version"><?php _e('Current Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_current_version" name="wpdd_current_version" value="<?php echo esc_attr($current_version); ?>" placeholder="1.0.0" />
</p>
<p>
<label for="wpdd_min_wp_version"><?php _e('Minimum WordPress Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_min_wp_version" name="wpdd_min_wp_version" value="<?php echo esc_attr($min_wp_version); ?>" placeholder="5.0" />
</p>
<p>
<label for="wpdd_tested_wp_version"><?php _e('Tested up to WordPress Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_tested_wp_version" name="wpdd_tested_wp_version" value="<?php echo esc_attr($tested_wp_version); ?>" placeholder="6.4" />
</p>
</div>
<script type="text/javascript">
function wpddRegenerateWebhookPasscode(productId) {
if (confirm('<?php _e('Are you sure? This will invalidate the existing webhook URL.', 'wp-digital-download'); ?>')) {
var newPasscode = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
jQuery('#wpdd_webhook_passcode').val(newPasscode);
var newUrl = '<?php echo home_url("/wp-json/wpdd/v1/webhook/"); ?>' + productId + '/' + newPasscode;
jQuery('input[readonly]').val(newUrl);
}
}
</script>
<?php
}
public static function render_settings_metabox($post) {
$download_limit = get_post_meta($post->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']));
}
}
}

View File

@@ -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);

View File

@@ -0,0 +1,169 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Setup {
public static function init() {
add_action('admin_notices', array(__CLASS__, 'show_setup_notice'));
add_action('wp_ajax_wpdd_dismiss_setup', array(__CLASS__, 'dismiss_setup_notice'));
add_action('wp_ajax_wpdd_create_pages', array(__CLASS__, 'create_default_pages'));
}
public static function show_setup_notice() {
if (!get_option('wpdd_show_setup_notice') || get_option('wpdd_setup_completed')) {
return;
}
if (!current_user_can('manage_options')) {
return;
}
$screen = get_current_screen();
if ($screen && strpos($screen->id, 'wpdd_product') === false && $screen->id !== 'dashboard') {
return;
}
?>
<div class="notice notice-info wpdd-setup-notice" style="position: relative;">
<h3><?php _e('🎉 WP Digital Download Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('Thank you for installing WP Digital Download! To get started, you can create the default pages or set them up manually.', 'wp-digital-download'); ?></p>
<div class="wpdd-setup-actions" style="margin-bottom: 10px;">
<button type="button" class="button button-primary" id="wpdd-create-pages">
<?php _e('Create Default Pages', 'wp-digital-download'); ?>
</button>
<button type="button" class="button button-secondary" id="wpdd-skip-setup">
<?php _e('Skip - I\'ll Set Up Manually', 'wp-digital-download'); ?>
</button>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings'); ?>" class="button">
<?php _e('Go to Settings', 'wp-digital-download'); ?>
</a>
</div>
<div class="wpdd-pages-preview" style="background: #f9f9f9; padding: 15px; margin: 10px 0; border-radius: 4px;">
<h4><?php _e('Default Pages to be Created:', 'wp-digital-download'); ?></h4>
<ul style="columns: 2; margin: 0;">
<li><strong><?php _e('Shop', 'wp-digital-download'); ?></strong> - <?php _e('Product catalog with filtering', 'wp-digital-download'); ?></li>
<li><strong><?php _e('My Purchases', 'wp-digital-download'); ?></strong> - <?php _e('Customer purchase history', 'wp-digital-download'); ?></li>
<li><strong><?php _e('Checkout', 'wp-digital-download'); ?></strong> - <?php _e('Payment and order form', 'wp-digital-download'); ?></li>
<li><strong><?php _e('Thank You', 'wp-digital-download'); ?></strong> - <?php _e('Order confirmation page', 'wp-digital-download'); ?></li>
</ul>
</div>
<div class="wpdd-setup-status" id="wpdd-setup-status" style="display: none; padding: 10px; margin: 10px 0; border-radius: 4px;">
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#wpdd-create-pages').on('click', function() {
var $button = $(this);
var $status = $('#wpdd-setup-status');
$button.prop('disabled', true).text('<?php _e('Creating Pages...', 'wp-digital-download'); ?>');
$status.show().removeClass().addClass('notice notice-info').html('<p><?php _e('Creating default pages...', 'wp-digital-download'); ?></p>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_create_pages',
nonce: '<?php echo wp_create_nonce('wpdd_setup_nonce'); ?>'
},
success: function(response) {
if (response.success) {
$status.removeClass().addClass('notice notice-success').html(
'<p><strong><?php _e('Success!', 'wp-digital-download'); ?></strong> ' + response.data.message + '</p>' +
'<ul>' + response.data.pages.map(function(page) {
return '<li><a href="' + page.edit_url + '">' + page.title + '</a> - <a href="' + page.view_url + '" target="_blank"><?php _e('View', 'wp-digital-download'); ?></a></li>';
}).join('') + '</ul>'
);
$('.wpdd-setup-actions').hide();
setTimeout(function() {
$('.wpdd-setup-notice').fadeOut();
}, 5000);
} else {
$status.removeClass().addClass('notice notice-error').html('<p><strong><?php _e('Error:', 'wp-digital-download'); ?></strong> ' + (response.data || '<?php _e('Unknown error occurred', 'wp-digital-download'); ?>') + '</p>');
$button.prop('disabled', false).text('<?php _e('Create Default Pages', 'wp-digital-download'); ?>');
}
},
error: function() {
$status.removeClass().addClass('notice notice-error').html('<p><strong><?php _e('Error:', 'wp-digital-download'); ?></strong> <?php _e('Failed to create pages. Please try again.', 'wp-digital-download'); ?></p>');
$button.prop('disabled', false).text('<?php _e('Create Default Pages', 'wp-digital-download'); ?>');
}
});
});
$('#wpdd-skip-setup').on('click', function() {
var $button = $(this);
$button.prop('disabled', true).text('<?php _e('Dismissing...', 'wp-digital-download'); ?>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_dismiss_setup',
nonce: '<?php echo wp_create_nonce('wpdd_setup_nonce'); ?>'
},
success: function(response) {
$('.wpdd-setup-notice').fadeOut();
}
});
});
});
</script>
<?php
}
public static function create_default_pages() {
check_ajax_referer('wpdd_setup_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied.', 'wp-digital-download'));
}
$created_pages = WPDD_Install::create_pages_optional();
if (!empty($created_pages)) {
$pages_info = array();
foreach ($created_pages as $page_id) {
$page = get_post($page_id);
if ($page) {
$pages_info[] = array(
'title' => $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();
}
}

View File

@@ -0,0 +1,471 @@
<?php
/**
* WPDD Plugin Updater Library
*
* Include this file in your WordPress plugin to enable automatic updates
* and license validation through the WP Digital Download licensing system.
*
* @version 1.0.0
* @author WP Digital Download
*/
if (!defined('ABSPATH')) {
exit;
}
if (!class_exists('WPDD_Plugin_Updater')) {
class WPDD_Plugin_Updater {
private $plugin_slug;
private $plugin_file;
private $version;
private $license_key;
private $update_server;
private $transient_key;
/**
* Initialize the updater
*
* @param string $plugin_file Full path to the main plugin file
* @param string $license_key Your license key
* @param string $update_server URL to your update server
* @param array $args Additional arguments
*/
public function __construct($plugin_file, $license_key, $update_server, $args = array()) {
$this->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();
?>
<div class="wrap">
<h1><?php printf(__('%s License Settings', 'default'), esc_html($this->plugin_slug)); ?></h1>
<?php settings_errors('wpdd_license'); ?>
<form method="post" action="">
<?php wp_nonce_field('wpdd_license_nonce', 'license_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="license_key"><?php _e('License Key', 'default'); ?></label>
</th>
<td>
<input type="text" id="license_key" name="license_key"
value="<?php echo esc_attr($license_key); ?>" class="regular-text" />
<?php if ($license_status): ?>
<span class="dashicons dashicons-yes-alt" style="color: green;"></span>
<span style="color: green;"><?php _e('Active', 'default'); ?></span>
<?php elseif (!empty($license_key)): ?>
<span class="dashicons dashicons-dismiss" style="color: red;"></span>
<span style="color: red;"><?php _e('Invalid/Expired', 'default'); ?></span>
<?php endif; ?>
</td>
</tr>
</table>
<?php if (empty($license_key) || !$license_status): ?>
<p class="submit">
<input type="submit" name="activate_license" class="button-primary"
value="<?php _e('Activate License', 'default'); ?>" />
</p>
<?php else: ?>
<p class="submit">
<input type="submit" name="deactivate_license" class="button-secondary"
value="<?php _e('Deactivate License', 'default'); ?>" />
</p>
<?php endif; ?>
</form>
<h2><?php _e('Instructions', 'default'); ?></h2>
<ol>
<li><?php _e('Enter your license key above and click "Activate License"', 'default'); ?></li>
<li><?php _e('Once activated, you will receive automatic updates for this plugin', 'default'); ?></li>
<li><?php _e('You can deactivate the license if you want to use it on a different site', 'default'); ?></li>
</ol>
</div>
<?php
}
/**
* Delete update transients
*/
public function delete_transients() {
delete_transient($this->transient_key);
delete_site_transient('update_plugins');
}
}
} // End class exists check

136
tests/webhook-samples.md Normal file
View File

@@ -0,0 +1,136 @@
# Gitea Webhook Integration Test Samples
The corrected webhook implementation now properly receives and processes webhook notifications FROM Git platforms like Gitea when new releases are published.
## Sample Gitea Release Webhook Payload
When a release is published in Gitea, it sends a webhook payload like this:
```json
{
"action": "published",
"release": {
"id": 123,
"tag_name": "v1.2.0",
"target_commitish": "main",
"name": "Version 1.2.0",
"body": "## What's New\n- Added new feature X\n- Fixed bug Y\n- Improved performance\n\n## Breaking Changes\n- None",
"url": "https://git.example.com/user/repo/releases/tag/v1.2.0",
"html_url": "https://git.example.com/user/repo/releases/tag/v1.2.0",
"tarball_url": "https://git.example.com/user/repo/archive/v1.2.0.tar.gz",
"zipball_url": "https://git.example.com/user/repo/archive/v1.2.0.zip",
"draft": false,
"prerelease": false,
"created_at": "2025-01-15T10:30:00Z",
"published_at": "2025-01-15T10:30:00Z",
"author": {
"id": 1,
"login": "developer",
"full_name": "Developer Name"
}
},
"repository": {
"id": 456,
"name": "my-wordpress-plugin",
"full_name": "user/my-wordpress-plugin",
"html_url": "https://git.example.com/user/my-wordpress-plugin",
"clone_url": "https://git.example.com/user/my-wordpress-plugin.git"
},
"sender": {
"id": 1,
"login": "developer"
}
}
```
## How the Webhook Handler Processes This
1. **Authentication**: Validates the passcode in the URL path
2. **Event Detection**: Identifies this as a Gitea release event (`action: "published"`)
3. **Version Extraction**: Extracts version "1.2.0" from `tag_name: "v1.2.0"`
4. **Changelog Processing**: Uses the release `body` field for changelog
5. **Package Creation**: Clones the repository at tag `v1.2.0` and creates distribution package
6. **Database Storage**: Stores the new version in `wpdd_software_versions` table
7. **Customer Updates**: Customers with valid licenses can now receive the update
## Sample GitHub Release Webhook Payload
GitHub uses a similar but slightly different structure:
```json
{
"action": "published",
"release": {
"tag_name": "v1.2.0",
"target_commitish": "main",
"name": "Version 1.2.0",
"body": "Release notes here...",
"draft": false,
"prerelease": false
},
"repository": {
"name": "my-plugin",
"clone_url": "https://github.com/user/my-plugin.git"
}
}
```
## Sample Git Push with Tag Webhook
For platforms that send tag push events instead of release events:
```json
{
"ref": "refs/tags/v1.2.0",
"repository": {
"clone_url": "https://git.example.com/user/repo.git"
},
"commits": [
{
"id": "abc123",
"message": "Release version 1.2.0"
}
],
"head_commit": {
"id": "abc123",
"message": "Release version 1.2.0"
},
"after": "abc123"
}
```
## Webhook URL Format
The webhook URLs generated for each software product follow this format:
```
https://streamers.channel/wp-json/wpdd/v1/webhook/{product_id}/{passcode}
```
Example:
```
https://streamers.channel/wp-json/wpdd/v1/webhook/123/a1b2c3d4e5f6
```
Where:
- `123` is the WordPress post ID of the software product
- `a1b2c3d4e5f6` is the randomly generated passcode for security
## Testing the Integration
To test this webhook integration:
1. Create a software product in WordPress admin
2. Set the product type to "Software License"
3. Configure the Git repository URL and credentials
4. Copy the generated webhook URL from the metabox
5. Add the webhook URL to your Gitea repository settings:
- Go to Settings → Webhooks
- Add new webhook with the WPDD URL
- Select "Release events" as the trigger
- Set Content-Type to "application/json"
6. Create and publish a new release in Gitea
7. Check the `wpdd_webhook_events` table to see the received payload
8. Check the `wpdd_software_versions` table to see the processed release
This corrected implementation properly receives release notifications FROM Git platforms like Gitea, rather than attempting to push to them.

View File

@@ -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 {