perform_order_admin_action($action, $order_id, $redirect_base); } public function enqueue_admin_styles(): void { $css_ver = ToDownload_VERSION; $css_path = ToDownload_PLUGIN_DIR . 'assets/admin-datepicker.css'; if (is_readable($css_path)) { $mtime = filemtime($css_path); if ($mtime) { $css_ver = (string) $mtime; } } $js_ver = ToDownload_VERSION; $js_path = ToDownload_PLUGIN_DIR . 'assets/admin-datepicker.js'; if (is_readable($js_path)) { $mtime = filemtime($js_path); if ($mtime) { $js_ver = (string) $mtime; } } wp_enqueue_style( 'todownload-admin-datepicker', ToDownload_PLUGIN_URL . 'assets/admin-datepicker.css', ['wp-jquery-ui-dialog'], $css_ver ); // Enables a bigger calendar popup (jQuery UI datepicker) when a field uses the // `.todownload-datepicker` class. Safe to enqueue broadly. wp_enqueue_script('jquery-ui-datepicker'); wp_enqueue_script( 'todownload-admin-datepicker', ToDownload_PLUGIN_URL . 'assets/admin-datepicker.js', ['jquery', 'jquery-ui-datepicker'], $js_ver, true ); $copy_ver = ToDownload_VERSION; $copy_path = ToDownload_PLUGIN_DIR . 'assets/admin-copy.js'; if (is_readable($copy_path)) { $mtime = filemtime($copy_path); if ($mtime) { $copy_ver = (string) $mtime; } } wp_enqueue_script( 'todownload-admin-copy', ToDownload_PLUGIN_URL . 'assets/admin-copy.js', [], $copy_ver, true ); wp_localize_script('todownload-admin-copy', 'ToDownloadAdminCopy', [ 'copy' => __('Copy', '2download-connector'), 'copied' => __('Copied!', '2download-connector'), 'copyFailed' => __('Copy failed', '2download-connector'), ]); $plan_ver = ToDownload_VERSION; $plan_path = ToDownload_PLUGIN_DIR . 'assets/admin-plan-discount.js'; if (is_readable($plan_path)) { $mtime = filemtime($plan_path); if ($mtime) { $plan_ver = (string) $mtime; } } wp_enqueue_script( 'todownload-admin-plan-discount', ToDownload_PLUGIN_URL . 'assets/admin-plan-discount.js', [], $plan_ver, true ); } public function handle_admin_create_plan_discount(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } if (!$this->ops_tools_enabled()) { wp_die(esc_html__('Ops tools are disabled on this site.', '2download-connector')); } check_admin_referer('ToDownload_admin_create_plan_discount'); $code = isset($_POST['code']) ? strtoupper(trim(sanitize_text_field((string) wp_unslash($_POST['code'])))) : ''; $duration = isset($_POST['duration']) ? sanitize_key((string) wp_unslash($_POST['duration'])) : 'once'; $duration_in_months = isset($_POST['duration_in_months']) ? absint(wp_unslash($_POST['duration_in_months'])) : 0; $percent_off = isset($_POST['percent_off']) ? (float) sanitize_text_field((string) wp_unslash($_POST['percent_off'])) : 0.0; $amount_off_usd = isset($_POST['amount_off_usd']) ? (float) sanitize_text_field((string) wp_unslash($_POST['amount_off_usd'])) : 0.0; $months_free = isset($_POST['months_free']) ? absint(wp_unslash($_POST['months_free'])) : 0; $max_redemptions = isset($_POST['max_redemptions']) ? absint(wp_unslash($_POST['max_redemptions'])) : 0; $expires_ymd = isset($_POST['expires_ymd']) ? sanitize_text_field((string) wp_unslash($_POST['expires_ymd'])) : ''; $restrict_tier = isset($_POST['restrict_tier']) ? sanitize_key((string) wp_unslash($_POST['restrict_tier'])) : 'both'; if ($code === '' || !preg_match('/^[A-Z0-9_-]{3,32}$/', $code)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Invalid code. Use 3–32 chars: A–Z, 0–9, _ or -.', '2download-connector')), ])); exit; } if ($duration !== 'once' && $duration !== 'forever' && $duration !== 'repeating') { $duration = 'once'; } $duration_in_months = max(0, min(36, (int) $duration_in_months)); $months_free = max(0, min(36, (int) $months_free)); $max_redemptions = max(0, min(1000000, (int) $max_redemptions)); $expires_ymd = trim((string) $expires_ymd); if ($expires_ymd !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $expires_ymd)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Expiration must be a YYYY-MM-DD date.', '2download-connector')), ])); exit; } // "Months free" is modeled as a repeating coupon with 100% off for N months. // (This keeps subscription billing entirely in Stripe.) if ($months_free > 0) { $duration = 'repeating'; $duration_in_months = $months_free; $percent_off = 100.0; $amount_off_usd = 0.0; } $has_percent = $percent_off > 0; $has_amount = $amount_off_usd > 0; if (($has_percent && $has_amount) || (!$has_percent && !$has_amount)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Enter either Percent off OR Amount off (USD), or set Months free.', '2download-connector')), ])); exit; } if ($duration === 'repeating' && $duration_in_months <= 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Repeating discounts require “Duration in months”.', '2download-connector')), ])); exit; } if ($has_percent) { $percent_off = max(0.01, min(100.0, (float) $percent_off)); } if ($has_amount) { $amount_off_usd = max(0.01, (float) $amount_off_usd); } $coupon_params = [ 'duration' => $duration, 'name' => '2DL Plan Code ' . $code, 'metadata[ToDownload_purpose]' => 'seller_plan_discount', 'metadata[ToDownload_code]' => $code, ]; if ($restrict_tier !== 'pro' && $restrict_tier !== 'studio' && $restrict_tier !== 'both') { $restrict_tier = 'both'; } $coupon_params['metadata[ToDownload_allowed_tiers]'] = $restrict_tier; // Restrict coupon to Pro/Studio products (Stripe-enforced). $allowed_products = []; if ($restrict_tier === 'pro' || $restrict_tier === 'both') { $pro_price = defined('TODOWNLOAD_STRIPE_PRICE_PRO_YEARLY') ? trim((string) TODOWNLOAD_STRIPE_PRICE_PRO_YEARLY) : ''; if ($pro_price === '' || strpos($pro_price, 'price_') !== 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Pro Stripe Price ID is not configured (TODOWNLOAD_STRIPE_PRICE_PRO_YEARLY).', '2download-connector')), ])); exit; } $pro_product = $this->stripe_product_id_for_price($pro_price); if ($pro_product === '') { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Could not resolve Pro Stripe Product from its Price ID.', '2download-connector')), ])); exit; } $allowed_products[$pro_product] = true; } if ($restrict_tier === 'studio' || $restrict_tier === 'both') { $studio_price = defined('TODOWNLOAD_STRIPE_PRICE_STUDIO_YEARLY') ? trim((string) TODOWNLOAD_STRIPE_PRICE_STUDIO_YEARLY) : ''; if ($studio_price === '' || strpos($studio_price, 'price_') !== 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Studio Stripe Price ID is not configured (TODOWNLOAD_STRIPE_PRICE_STUDIO_YEARLY).', '2download-connector')), ])); exit; } $studio_product = $this->stripe_product_id_for_price($studio_price); if ($studio_product === '') { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Could not resolve Studio Stripe Product from its Price ID.', '2download-connector')), ])); exit; } $allowed_products[$studio_product] = true; } $allowed_products = array_keys($allowed_products); if (empty($allowed_products)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Select Pro, Studio, or Both for plan restriction.', '2download-connector')), ])); exit; } foreach (array_values($allowed_products) as $i => $prod_id) { $coupon_params['applies_to[products][' . (int) $i . ']'] = (string) $prod_id; } if ($duration === 'repeating') { $coupon_params['duration_in_months'] = (string) $duration_in_months; } if ($has_percent) { $coupon_params['percent_off'] = (string) $percent_off; } else { $coupon_params['amount_off'] = (string) ((int) round($amount_off_usd * 100)); $coupon_params['currency'] = 'usd'; } $coupon_res = $this->stripe_request('POST', '/v1/coupons', $coupon_params, 'todownload_plan_coupon_' . strtolower($code)); if (empty($coupon_res['ok'])) { $msg = (string) ($coupon_res['message'] ?? __('Stripe request failed.', '2download-connector')); wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode($msg), ])); exit; } $coupon = (array) ($coupon_res['data'] ?? []); $coupon_id = isset($coupon['id']) ? (string) $coupon['id'] : ''; if ($coupon_id === '' || strpos($coupon_id, 'coupon_') !== 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Stripe coupon ID missing.', '2download-connector')), ])); exit; } $promo_params = [ 'coupon' => $coupon_id, 'code' => $code, 'active' => 'true', 'metadata[ToDownload_purpose]' => 'seller_plan_discount', ]; if ($max_redemptions > 0) { $promo_params['max_redemptions'] = (string) $max_redemptions; } if ($expires_ymd !== '') { // Stripe expects a Unix timestamp. Use end-of-day UTC to match admin expectations. $ts = strtotime($expires_ymd . ' 23:59:59 UTC'); if (!$ts) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode(__('Could not parse expiration date.', '2download-connector')), ])); exit; } $promo_params['expires_at'] = (string) ((int) $ts); } $promo_res = $this->stripe_request('POST', '/v1/promotion_codes', $promo_params, 'todownload_plan_promo_' . strtolower($code)); if (empty($promo_res['ok'])) { $msg = (string) ($promo_res['message'] ?? __('Stripe request failed.', '2download-connector')); wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_error' => rawurlencode($msg), ])); exit; } /* translators: 1: Stripe promotion code name */ $notice_msg = sprintf(__('Created Stripe promotion code: %1$s', '2download-connector'), $code); if ($months_free > 0) { /* translators: 1: Stripe promotion code name, 2: number of months free */ $notice_msg = sprintf(__('Created Stripe promotion code: %1$s (%2$d months free)', '2download-connector'), $code, (int) $months_free); } wp_safe_redirect($this->settings_url([ 'ToDownload_admin_plan_discount_notice' => rawurlencode($notice_msg), ])); exit; } /** * @return array{ok:bool, data?:array, message?:string} */ private function stripe_request(string $method, string $path, array $params = [], ?string $idempotency_key = null): array { $secret = $this->platform_stripe_secret_key(); if ($secret === '') { return ['ok' => false, 'message' => __('This site is configured as a 2dl.app connector. Stripe secrets are not stored on seller sites.', '2download-connector')]; } // Connector invariant: seller sites must not call payment processor APIs directly. // Stripe operations are handled centrally by 2dl.app. return ['ok' => false, 'message' => __('Stripe operations are handled centrally by 2dl.app for connector sites.', '2download-connector')]; } private function platform_stripe_secret_key(): string { return ''; } private function stripe_product_id_for_price(string $price_id): string { $price_id = trim((string) $price_id); if ($price_id === '' || strpos($price_id, 'price_') !== 0) { return ''; } $res = $this->stripe_request('GET', '/v1/prices/' . rawurlencode($price_id), [], 'todownload_get_price_' . $price_id); if (empty($res['ok'])) { return ''; } $price = (array) ($res['data'] ?? []); if (empty($price['product'])) { return ''; } if (is_string($price['product'])) { $prod = trim((string) $price['product']); return (strpos($prod, 'prod_') === 0) ? $prod : ''; } if (is_array($price['product'])) { $prod = isset($price['product']['id']) ? trim((string) $price['product']['id']) : ''; return (strpos($prod, 'prod_') === 0) ? $prod : ''; } return ''; } public function handle_admin_grant_access(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } if (!$this->ops_tools_enabled()) { wp_die(esc_html__('Ops tools are disabled on this site.', '2download-connector')); } check_admin_referer('ToDownload_admin_grant_access'); $grant_mode = isset($_POST['grant_mode']) ? sanitize_key((string) wp_unslash($_POST['grant_mode'])) : ''; if ($grant_mode !== 'plan' && $grant_mode !== 'download') { $grant_mode = 'download'; } $plan_tier = isset($_POST['plan_tier']) ? sanitize_key((string) wp_unslash($_POST['plan_tier'])) : ''; $plan_duration = isset($_POST['plan_duration']) ? sanitize_key((string) wp_unslash($_POST['plan_duration'])) : ''; $user_id = isset($_POST['user_id']) ? absint(wp_unslash($_POST['user_id'])) : 0; $email = isset($_POST['customer_email']) ? sanitize_email((string) wp_unslash($_POST['customer_email'])) : ''; $email = trim($email); // Seller plan grant (admin-only): grant Pro/Studio for a fixed duration. if ($grant_mode === 'plan') { if ($plan_tier !== Pricing::TIER_PRO && $plan_tier !== Pricing::TIER_STUDIO) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('Pick a Seller plan tier (Pro or Studio).', '2download-connector')), ])); exit; } // Resolve seller user. $seller = null; if ($user_id > 0) { $seller = get_user_by('id', $user_id); } if (!($seller instanceof \WP_User) && $email !== '' && is_email($email)) { $seller = get_user_by('email', $email); if ($seller instanceof \WP_User) { $user_id = (int) $seller->ID; } } if (!($seller instanceof \WP_User) || $user_id <= 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('Seller user not found. Provide a valid Seller User ID or an email that belongs to an existing seller user.', '2download-connector')), ])); exit; } if (!user_can($seller, 'edit_todownload_downloads')) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('Selected user is not a seller.', '2download-connector')), ])); exit; } $expires = ''; $duration_label = ''; if ($plan_duration === 'life') { $expires = ''; $duration_label = __('Free for life', '2download-connector'); } else { $map = [ 'm1' => ['P1M', __('1 month', '2download-connector')], 'm3' => ['P3M', __('3 months', '2download-connector')], 'm6' => ['P6M', __('6 months', '2download-connector')], 'm12' => ['P1Y', __('12 months', '2download-connector')], 'y1' => ['P1Y', __('1 year', '2download-connector')], 'y2' => ['P2Y', __('2 years', '2download-connector')], 'y3' => ['P3Y', __('3 years', '2download-connector')], 'y5' => ['P5Y', __('5 years', '2download-connector')], ]; $spec = $map[$plan_duration] ?? $map['y1']; $duration_label = (string) ($spec[1] ?? ''); try { $dt = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); $dt2 = $dt->add(new \DateInterval((string) ($spec[0] ?? 'P1Y'))); $expires = (string) $dt2->format('Y-m-d'); } catch (\Throwable $e) { $expires = ''; } } Pricing::set_seller_tier((int) $user_id, (string) $plan_tier, (string) $expires); // If a plan product is configured, also create a $0 completed order for audit/testing. $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $plan_product_id = 0; if ($plan_tier === Pricing::TIER_PRO) { $plan_product_id = (int) ($settings['seller_plan_pro_product_id'] ?? 0); } else if ($plan_tier === Pricing::TIER_STUDIO) { $plan_product_id = (int) ($settings['seller_plan_studio_product_id'] ?? 0); } $plan_product_id = max(0, (int) $plan_product_id); if ($plan_product_id > 0 && (string) get_post_type($plan_product_id) === DownloadPostType::POST_TYPE) { $seller_email = sanitize_email((string) ($seller->user_email ?? '')); $seller_email = ($seller_email !== '' && is_email($seller_email)) ? $seller_email : $email; $customer_id = Orders::create_customer([ 'user_id' => (int) $user_id, 'email' => $seller_email, 'first_name' => (string) ($seller->user_firstname ?? ''), 'last_name' => (string) ($seller->user_lastname ?? ''), ]); $order_id = Orders::create_order([ 'customer_id' => $customer_id, 'seller_user_id' => 0, 'status' => 'complete', 'subtotal' => 0.0, 'tax' => 0.0, 'discount' => 0.0, 'total' => 0.0, 'gateway' => 'admin_grant', ]); Orders::append_order_note($order_id, 'ToDownload_admin_grant_access: plan_tier=' . $plan_tier . ' duration=' . $plan_duration . ' expires=' . $expires . ' seller_user_id=' . (int) $user_id); Orders::add_order_item($order_id, (int) $plan_product_id, 0.0); Orders::update_status($order_id, 'complete', 'admin_grant_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); } $tier_label = $plan_tier === Pricing::TIER_STUDIO ? __('Studio', '2download-connector') : __('Pro', '2download-connector'); if ($duration_label !== '') { /* translators: 1: plan tier (Studio/Pro), 2: duration label */ $msg = sprintf(__('Granted %1$s plan (%2$s).', '2download-connector'), (string) $tier_label, (string) $duration_label); } else { /* translators: %s = plan tier (Studio/Pro) */ $msg = sprintf(__('Granted %s plan.', '2download-connector'), (string) $tier_label); } if ($expires !== '') { /* translators: %s = expiration date (YYYY-MM-DD) */ $msg .= ' ' . sprintf(__('Expires: %s', '2download-connector'), (string) $expires); } wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_notice' => rawurlencode($msg), ])); exit; } // Download access grant ($0 order) mode. // Ensure we don't accidentally interpret stale plan fields. $plan_tier = ''; $plan_duration = ''; if ($user_id > 0) { $u = get_user_by('id', $user_id); if (!($u instanceof \WP_User)) { // If an email was said provided, proceed as a guest customer instead of hard-failing. // This matches the intended usage: email-only should work. if ($email === '' || !is_email($email)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('User not found (and no valid email provided).', '2download-connector')), ])); exit; } $user_id = 0; $u = null; } if ($u instanceof \WP_User) { $u_email = sanitize_email((string) ($u->user_email ?? '')); if ($u_email !== '' && is_email($u_email)) { $email = $u_email; } } } if ($email === '' || !is_email($email)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('Valid email is required (or provide a valid user ID).', '2download-connector')), ])); exit; } $download_ids_raw = isset($_POST['download_ids']) ? sanitize_text_field((string) wp_unslash($_POST['download_ids'])) : ''; $download_ids_raw = trim($download_ids_raw); $ids = []; if ($download_ids_raw !== '') { foreach (preg_split('/\s*,\s*/', $download_ids_raw) ?: [] as $p) { $p = trim((string) $p); if ($p === '') { continue; } $id = (int) $p; if ($id > 0) { $ids[$id] = true; } } } $download_ids = array_keys($ids); if (empty($download_ids)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('Provide one or more product IDs.', '2download-connector')), ])); exit; } // Validate products. $valid_ids = []; $invalid_ids = []; foreach ($download_ids as $did) { $did = (int) $did; if ($did <= 0) { continue; } if ((string) get_post_type($did) !== DownloadPostType::POST_TYPE) { $invalid_ids[] = $did; continue; } $status = (string) get_post_status($did); if (!in_array($status, ['publish', 'private'], true)) { $invalid_ids[] = $did; continue; } $valid_ids[] = $did; } $valid_ids = array_values(array_unique($valid_ids)); if (empty($valid_ids)) { $suffix = ''; if (!empty($invalid_ids)) { /* translators: %s = comma-separated list of invalid IDs */ $suffix = ' ' . sprintf(__('Invalid/unsupported IDs: %s', '2download-connector'), implode(',', array_map('intval', array_values(array_unique($invalid_ids))))); } wp_safe_redirect($this->settings_url([ 'ToDownload_admin_grant_error' => rawurlencode(__('No valid products found for the provided IDs. Use Product post IDs from 2DL → Products (Published/Private).', '2download-connector') . $suffix), ])); exit; } $first_name = ''; $last_name = ''; if ($user_id > 0) { $u = get_user_by('id', $user_id); if ($u instanceof \WP_User) { $first_name = (string) ($u->user_firstname ?? ''); $last_name = (string) ($u->user_lastname ?? ''); } } $customer_id = Orders::create_customer([ 'user_id' => $user_id > 0 ? $user_id : null, 'email' => $email, 'first_name' => $first_name, 'last_name' => $last_name, ]); $order_id = Orders::create_order([ 'customer_id' => $customer_id, // Admin grants are platform orders. 'seller_user_id' => 0, 'status' => 'complete', 'subtotal' => 0.0, 'tax' => 0.0, 'discount' => 0.0, 'total' => 0.0, 'gateway' => 'admin_grant', ]); Orders::append_order_note($order_id, 'ToDownload_admin_grant_access: admin_user_id=' . (int) get_current_user_id() . ' email=' . $email); foreach ($valid_ids as $did) { Orders::add_order_item($order_id, (int) $did, 0.0); } Orders::update_status($order_id, 'complete', 'admin_grant_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); wp_safe_redirect($this->settings_url([ /* translators: %d = order ID */ 'ToDownload_admin_grant_notice' => rawurlencode(sprintf(__('Download access granted. Order #%d created.', '2download-connector'), (int) $order_id)), ])); exit; } public function maybe_handle_orders_page_actions(): void { if (!is_admin() || !current_user_can('manage_options')) { return; } $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if ($page !== 'ToDownload_admin') { return; } // Save/clear per-admin default seller filter. $set_default = isset($_GET['ToDownload_orders_set_default_seller']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_orders_set_default_seller'])) : ''; if ($set_default === '1') { $nonce = isset($_GET['ToDownload_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_nonce'])) : ''; if (!$nonce || !wp_verify_nonce($nonce, 'ToDownload_orders_set_default_seller')) { wp_die(esc_html__('Invalid request.', '2download-connector')); } $seller_user_id = isset($_GET['ToDownload_seller_user_id']) ? absint(wp_unslash($_GET['ToDownload_seller_user_id'])) : 0; if ($seller_user_id > 0) { update_user_meta((int) get_current_user_id(), 'ToDownload_default_orders_seller_user_id', $seller_user_id); } $redirect = admin_url('admin.php?page=ToDownload_admin'); $redirect = add_query_arg([ 'ToDownload_orders_notice' => 'default_saved', ], $redirect); wp_safe_redirect($redirect); exit; } $clear_default = isset($_GET['ToDownload_orders_clear_default_seller']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_orders_clear_default_seller'])) : ''; if ($clear_default === '1') { $nonce = isset($_GET['ToDownload_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_nonce'])) : ''; if (!$nonce || !wp_verify_nonce($nonce, 'ToDownload_orders_clear_default_seller')) { wp_die(esc_html__('Invalid request.', '2download-connector')); } delete_user_meta((int) get_current_user_id(), 'ToDownload_default_orders_seller_user_id'); $redirect = admin_url('admin.php?page=ToDownload_admin'); $redirect = add_query_arg([ 'ToDownload_orders_notice' => 'default_cleared', ], $redirect); wp_safe_redirect($redirect); exit; } // Handle mark-complete/refund early (before any admin output). $this->handle_order_admin_actions(); // Handle CSV export early (before any output), otherwise headers will already be sent. $export = isset($_GET['ToDownload_export_orders']) ? sanitize_key((string) wp_unslash($_GET['ToDownload_export_orders'])) : ''; if ($export === '1') { $nonce = isset($_GET['ToDownload_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_nonce'])) : ''; if (!$nonce || !wp_verify_nonce($nonce, 'ToDownload_export_orders')) { wp_die(esc_html__('Invalid request.', '2download-connector')); } if (headers_sent()) { wp_die(esc_html__('Cannot export CSV because headers were already sent.', '2download-connector')); } $q = isset($_GET['ToDownload_q']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_q'])) : ''; $status_filter = isset($_GET['ToDownload_status']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_status'])) : ''; $seller_user_id = isset($_GET['ToDownload_seller_user_id']) ? absint(wp_unslash($_GET['ToDownload_seller_user_id'])) : 0; $rows = Orders::list_orders_filtered(2000, 0, [ 'q' => $q, 'status' => $status_filter, 'seller_user_id' => $seller_user_id, ]); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="2download-orders.csv"'); $fh = fopen('php://output', 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fputcsv($fh, ['id', 'created_at', 'status', 'customer_email', 'seller_user_id', 'total', 'gateway', 'transaction_id']); foreach ($rows as $o) { fputcsv($fh, [ (int) ($o['id'] ?? 0), (string) ($o['created_at'] ?? ''), (string) ($o['status'] ?? ''), (string) ($o['customer_email'] ?? ''), (int) ($o['seller_user_id'] ?? 0), (string) number_format((float) ($o['total'] ?? 0), 2, '.', ''), (string) ($o['gateway'] ?? ''), (string) ($o['transaction_id'] ?? ''), ]); } fclose($fh); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } } private function settings_url(array $args = []): string { $base = admin_url('admin.php?page=ToDownload_settings'); return !empty($args) ? (string) add_query_arg($args, $base) : (string) $base; } private function connect_url(array $args = []): string { $base = admin_url('admin.php?page=ToDownload_connect'); return !empty($args) ? (string) add_query_arg($args, $base) : (string) $base; } public function render_connect(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $hosted_base = trim((string) ($settings['hosted_api_base_url'] ?? 'https://2dl.app')); $hosted_store = trim((string) ($settings['hosted_store_public_id'] ?? '')); $hosted_key_configured = trim((string) ($settings['hosted_store_api_key'] ?? '')) !== ''; $is_prod = ($hosted_base === '' || $hosted_base === 'https://2dl.app' || $hosted_base === 'https://www.2dl.app'); $is_staging = ($hosted_base === 'https://staging.2dl.app'); echo '
'; echo '

' . esc_html__('Connect to 2dl.app', '2download-connector') . '

'; echo '

' . esc_html__('Configure your store connection so this site can create hosted checkouts and fetch receipts/licenses from 2dl.app.', '2download-connector') . '

'; // Read-only GET parameter used for showing a notice. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $settings_updated = isset($_GET['settings-updated']) ? sanitize_key((string) wp_unslash($_GET['settings-updated'])) : ''; if ($settings_updated !== '') { echo '

' . esc_html__('Connection settings saved.', '2download-connector') . '

'; } if ($hosted_store === '' || !$hosted_key_configured) { echo '

' . esc_html__('Not connected yet. Enter your Store public ID and Store API key.', '2download-connector') . '

'; } echo '
'; echo ''; echo ''; wp_nonce_field('ToDownload_save_settings', 'ToDownload_settings_nonce'); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; submit_button(__('Save connection', '2download-connector')); echo '
'; echo '

' . esc_html__('Advanced settings', '2download-connector') . '

'; echo '
'; } public function handle_admin_test_plan_purchase(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } if (!$this->ops_tools_enabled()) { wp_die(esc_html__('Ops tools are disabled on this site.', '2download-connector')); } check_admin_referer('ToDownload_admin_test_plan_purchase'); $tier = isset($_POST['tier']) ? sanitize_key((string) wp_unslash($_POST['tier'])) : ''; if ($tier !== Pricing::TIER_PRO && $tier !== Pricing::TIER_STUDIO) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Invalid tier.', '2download-connector')), ])); exit; } $seller_user_id = isset($_POST['seller_user_id']) ? absint(wp_unslash($_POST['seller_user_id'])) : 0; $seller_user_id = max(0, $seller_user_id); $seller = $seller_user_id > 0 ? get_user_by('id', $seller_user_id) : null; if (!($seller instanceof \WP_User) || $seller_user_id <= 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Seller user not found.', '2download-connector')), ])); exit; } if (!user_can($seller, 'edit_todownload_downloads')) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Selected user is not a seller.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $download_id = 0; if ($tier === Pricing::TIER_PRO) { $download_id = (int) ($settings['seller_plan_pro_product_id'] ?? 0); } else if ($tier === Pricing::TIER_STUDIO) { $download_id = (int) ($settings['seller_plan_studio_product_id'] ?? 0); } $download_id = max(0, $download_id); if ($download_id <= 0) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Plan product ID is not configured for this tier.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } if ((string) get_post_type($download_id) !== DownloadPostType::POST_TYPE || (string) get_post_status($download_id) !== 'publish') { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Plan product must be a published Product.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } $email = sanitize_email((string) ($seller->user_email ?? '')); if ($email === '' || !is_email($email)) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Seller email is missing/invalid.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } $price = (float) get_post_meta($download_id, '_ToDownload_price', true); $subtotal = max(0.0, (float) $price); $tax = 0.0; $discount = 0.0; $total = max(0.0, $subtotal + $tax - $discount); $customer_id = Orders::create_customer([ 'user_id' => (int) $seller_user_id, 'email' => $email, 'first_name' => (string) ($seller->user_firstname ?? ''), 'last_name' => (string) ($seller->user_lastname ?? ''), ]); $order_id = Orders::create_order([ 'customer_id' => $customer_id, // Platform order (membership purchases are not routed to seller Connect). 'seller_user_id' => 0, 'status' => 'pending', 'subtotal' => $subtotal, 'tax' => $tax, 'discount' => $discount, 'total' => $total, 'gateway' => $total > 0 ? 'stripe_checkout' : 'free', ]); Orders::append_order_note($order_id, 'ToDownload_admin_test_plan_purchase: tier=' . $tier . ' seller_user_id=' . (int) $seller_user_id); Orders::add_order_item($order_id, $download_id, (float) $price); if ($total <= 0) { Orders::update_status($order_id, 'complete', 'free_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_notice' => rawurlencode(__('Test plan activated (free).', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } $gateway = GatewayRegistry::instance()->get('stripe_checkout'); if (!$gateway) { wp_safe_redirect($this->settings_url([ 'ToDownload_admin_test_plan_error' => rawurlencode(__('Stripe Checkout gateway is not available.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ])); exit; } $success = $this->settings_url([ 'ToDownload_admin_test_plan_notice' => rawurlencode(__('Payment received. Waiting for Stripe to complete the order.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ]); $cancel = $this->settings_url([ 'ToDownload_admin_test_plan_notice' => rawurlencode(__('Test checkout cancelled.', '2download-connector')), 'seller_user_id' => (int) $seller_user_id, ]); $redirect = (string) $gateway->start_payment($order_id, [ 'email' => $email, 'success_url' => $success, 'cancel_url' => $cancel, ]); wp_safe_redirect($redirect); exit; } public function menu(): void { add_menu_page( __('2DL Admin', '2download-connector'), __('2DL Admin', '2download-connector'), 'manage_options', 'ToDownload_admin', [$this, 'render_orders'], 'dashicons-download', 56 ); add_submenu_page('ToDownload_admin', __('Orders', '2download-connector'), __('Orders', '2download-connector'), 'manage_options', 'ToDownload_admin', [$this, 'render_orders']); add_submenu_page('ToDownload_admin', __('Customers', '2download-connector'), __('Customers', '2download-connector'), 'manage_options', 'ToDownload_customers', [$this, 'render_customers']); add_submenu_page('ToDownload_admin', __('Seller Tiers', '2download-connector'), __('Seller Tiers', '2download-connector'), 'manage_options', 'ToDownload_seller_tiers', [$this, 'render_seller_tiers']); add_submenu_page('ToDownload_admin', __('Webhooks', '2download-connector'), __('Webhooks', '2download-connector'), 'manage_options', 'ToDownload_webhooks', [$this, 'render_webhooks']); add_submenu_page('ToDownload_admin', __('Connect', '2download-connector'), __('Connect', '2download-connector'), 'manage_options', 'ToDownload_connect', [$this, 'render_connect']); add_submenu_page('ToDownload_admin', __('Settings', '2download-connector'), __('Settings', '2download-connector'), 'manage_options', 'ToDownload_settings', [$this, 'render_settings']); } private function tier_label(string $tier): string { $tier = Pricing::normalize_tier($tier); if ($tier === Pricing::TIER_PRO) { return __('Pro', '2download-connector'); } if ($tier === Pricing::TIER_STUDIO) { return __('Studio', '2download-connector'); } return __('Standard', '2download-connector'); } public function render_seller_tiers(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } // Collect sellers (role-based) + any user that has a tier meta (covers edge cases). $seen = []; $users = []; $role_users = get_users([ 'role__in' => [Sellers::ROLE, 'administrator'], 'number' => 500, 'orderby' => 'ID', 'order' => 'ASC', ]); if (is_array($role_users)) { foreach ($role_users as $u) { if (!($u instanceof \WP_User)) { continue; } $uid = (int) $u->ID; if ($uid <= 0 || isset($seen[$uid])) { continue; } $seen[$uid] = true; $users[] = $u; } } $meta_users = get_users([ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Admin-only view; bounded query. 'meta_key' => Pricing::USER_META_TIER, 'number' => 500, 'orderby' => 'ID', 'order' => 'ASC', ]); if (is_array($meta_users)) { foreach ($meta_users as $u) { if (!($u instanceof \WP_User)) { continue; } $uid = (int) $u->ID; if ($uid <= 0 || isset($seen[$uid])) { continue; } $seen[$uid] = true; $users[] = $u; } } $rows = []; foreach ($users as $u) { $uid = (int) $u->ID; if ($uid <= 0) { continue; } if (!user_can($u, 'edit_todownload_downloads')) { continue; } $tier = Pricing::get_seller_tier($uid); $expires = (string) get_user_meta($uid, Pricing::USER_META_TIER_EXPIRES_AT, true); $expires = trim($expires); $rows[] = [ 'id' => $uid, 'name' => (string) ($u->display_name ?? ''), 'email' => (string) ($u->user_email ?? ''), 'tier' => $tier, 'expires' => $expires, 'platform_fee' => Pricing::platform_fee_percent_for_seller($uid), 'seller_pays_stripe' => !Pricing::stripe_fees_included_for_seller($uid), ]; } usort($rows, function ($a, $b) { $an = strtolower((string) ($a['name'] ?? '')); $bn = strtolower((string) ($b['name'] ?? '')); return $an <=> $bn; }); echo '
'; echo '

' . esc_html__('Seller Tiers', '2download-connector') . '

'; echo '

' . esc_html__('Operational view of current seller tiers and expirations.', '2download-connector') . '

'; if (empty($rows)) { echo '

' . esc_html__('No sellers found.', '2download-connector') . '

'; echo '
'; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($rows as $r) { $uid = (int) ($r['id'] ?? 0); $edit = admin_url('user-edit.php?user_id=' . $uid); $name = (string) ($r['name'] ?? ''); $email = (string) ($r['email'] ?? ''); $tier = (string) ($r['tier'] ?? Pricing::TIER_STARTER); $expires = (string) ($r['expires'] ?? ''); $platform_fee = (float) ($r['platform_fee'] ?? 0); $seller_pays_stripe = !empty($r['seller_pays_stripe']); $seller_label = trim($name . ($email !== '' ? ' <' . $email . '>' : '')); if ($seller_label === '') { $seller_label = '#' . (string) $uid; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Seller', '2download-connector') . '' . esc_html__('Tier', '2download-connector') . '' . esc_html__('Expires', '2download-connector') . '' . esc_html__('Platform fee', '2download-connector') . '' . esc_html__('Stripe fees', '2download-connector') . '
' . esc_html($seller_label) . '' . esc_html($this->tier_label($tier)) . '' . esc_html($expires !== '' ? $expires : '—') . '' . esc_html(rtrim(rtrim(number_format($platform_fee, 2, '.', ''), '0'), '.')) . '%' . esc_html($seller_pays_stripe ? __('Seller-paid', '2download-connector') : __('Included', '2download-connector')) . '
'; echo ''; } public function handle_clear_webhook_logs(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_clear_webhook_logs', 'ToDownload_nonce'); WebhookLog::clear(); $target = add_query_arg( [ 'page' => 'ToDownload_webhooks', 'cleared' => '1', 'ToDownload_webhooks_notice_nonce' => wp_create_nonce('ToDownload_webhooks_cleared'), ], admin_url('admin.php') ); wp_safe_redirect($target); exit; } public function render_webhooks(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $cleared = false; if (!empty($_GET['cleared']) && sanitize_key((string) wp_unslash($_GET['cleared'])) === '1') { $notice_nonce = isset($_GET['ToDownload_webhooks_notice_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_webhooks_notice_nonce'])) : ''; if ($notice_nonce !== '' && wp_verify_nonce($notice_nonce, 'ToDownload_webhooks_cleared')) { $cleared = true; } } if ($cleared) { echo '

' . esc_html__('Webhook logs cleared.', '2download-connector') . '

'; } echo '

' . esc_html__('Webhooks', '2download-connector') . '

'; echo '

' . esc_html__('Recent gateway webhook events (best-effort logging for debugging).', '2download-connector') . '

'; $clear_url = admin_url('admin-post.php?action=ToDownload_clear_webhook_logs'); $clear_url = wp_nonce_url($clear_url, 'ToDownload_clear_webhook_logs', 'ToDownload_nonce'); echo '

' . esc_html__('Clear logs', '2download-connector') . '

'; $filter_nonce = isset($_GET['ToDownload_webhooks_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_webhooks_nonce'])) : ''; $filter_ok = ($filter_nonce !== '' && wp_verify_nonce($filter_nonce, 'ToDownload_webhooks_filter')); $order_filter = ($filter_ok && isset($_GET['ToDownload_order_id'])) ? absint(wp_unslash($_GET['ToDownload_order_id'])) : 0; echo '
'; echo ''; echo wp_kses_post(wp_nonce_field('ToDownload_webhooks_filter', 'ToDownload_webhooks_nonce', true, false)); echo ''; echo ' '; submit_button(__('Filter', '2download-connector'), 'secondary', '', false); if ($order_filter > 0) { $clear_filter_url = admin_url('admin.php?page=ToDownload_webhooks'); echo ' ' . esc_html__('Clear filter', '2download-connector') . ''; } echo '
'; $rows = WebhookLog::all(200); if ($order_filter > 0) { $rows = array_values(array_filter($rows, function ($r) use ($order_filter) { return (int) ($r['order_id'] ?? 0) === (int) $order_filter; })); } if (empty($rows)) { echo '

' . esc_html__('No webhook events logged yet.', '2download-connector') . '

'; echo '
'; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($rows as $r) { $ts = (string) ($r['ts'] ?? ''); $gateway = (string) ($r['gateway'] ?? ''); $type = (string) ($r['type'] ?? ''); $order_id = (int) ($r['order_id'] ?? 0); $event_id = (string) ($r['event_id'] ?? ''); $message = (string) ($r['message'] ?? ''); $order_cell = '—'; if ($order_id > 0) { $jump_url = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_q' => $order_id, ], admin_url('admin.php')); $order_cell = '#' . esc_html((string) $order_id) . ''; } $event_cell = esc_html($event_id !== '' ? $event_id : '—'); if ($event_id !== '') { $event_cell = '' . esc_html($event_id) . ''; } $message_cell = esc_html($message !== '' ? $message : '—'); if ($message !== '') { $snippet = wp_trim_words($message, 14, '…'); $message_cell = esc_html($snippet) . ' '; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Time', '2download-connector') . '' . esc_html__('Gateway', '2download-connector') . '' . esc_html__('Type', '2download-connector') . '' . esc_html__('Order', '2download-connector') . '' . esc_html__('Event', '2download-connector') . '' . esc_html__('Message', '2download-connector') . '
' . esc_html($ts !== '' ? $ts : '—') . '' . esc_html($gateway !== '' ? $gateway : '—') . '' . esc_html($type !== '' ? $type : '—') . '' . wp_kses_post($order_cell) . '' . wp_kses_post($event_cell) . '' . wp_kses_post($message_cell) . '
'; echo ''; } private function handle_order_admin_actions(): void { if (!current_user_can('manage_options')) { return; } $action = isset($_GET['ToDownload_order_action']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_order_action'])) : ''; $order_id = isset($_GET['ToDownload_order_id']) ? absint(wp_unslash($_GET['ToDownload_order_id'])) : 0; $nonce = isset($_GET['ToDownload_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_nonce'])) : ''; if ($action === '' || $order_id <= 0) { return; } if (headers_sent()) { wp_die(esc_html__('Cannot perform this action because headers were already sent.', '2download-connector')); } if (!$nonce || !wp_verify_nonce($nonce, 'ToDownload_order_action_' . $order_id)) { wp_die(esc_html__('Invalid request.', '2download-connector')); } $redirect_base = remove_query_arg(['ToDownload_order_action', 'ToDownload_order_id', 'ToDownload_nonce']); $this->perform_order_admin_action($action, $order_id, (string) $redirect_base); } private function perform_order_admin_action(string $action, int $order_id, string $redirect_base): void { $redirect_base = $redirect_base !== '' ? $redirect_base : admin_url('admin.php?page=ToDownload_admin'); // Ensure notices/filters remain readable after internal redirects. $redirect_base = (string) add_query_arg([ self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], $redirect_base); $redirect_base = (string) remove_query_arg(['ToDownload_notice', 'ToDownload_msg'], $redirect_base); if ($action === 'complete') { Orders::update_status($order_id, 'complete'); do_action('ToDownload_after_order_complete', $order_id); Orders::append_order_note($order_id, 'ToDownload_admin_action: marked complete'); wp_safe_redirect($redirect_base); exit; } if ($action !== 'refund') { wp_safe_redirect($redirect_base); exit; } $order = Orders::get_order($order_id); if (!$order) { wp_safe_redirect(add_query_arg([ 'ToDownload_notice' => 'refund_failed', 'ToDownload_msg' => rawurlencode(__('Order not found.', '2download-connector')), ], $redirect_base)); exit; } $gateway_id = (string) ($order['gateway'] ?? ''); $txn = (string) ($order['transaction_id'] ?? ''); $total = (float) ($order['total'] ?? 0); $currency = (string) ($order['currency'] ?? 'USD'); if ($gateway_id === 'stripe_checkout') { $gateway = GatewayRegistry::instance()->get('stripe_checkout'); if ($gateway instanceof RefundableGatewayInterface) { $result = $gateway->refund_order($order_id, $txn, $total, $currency); if (!empty($result['ok'])) { Orders::update_status($order_id, 'refunded'); $refund_id = isset($result['refund_id']) ? (string) $result['refund_id'] : ''; $amount = isset($result['amount']) ? (int) $result['amount'] : null; $cur = isset($result['currency']) ? (string) $result['currency'] : strtoupper($currency); Orders::append_order_note($order_id, 'ToDownload_admin_action: refunded via Stripe'); if ($refund_id !== '') { Orders::append_order_note($order_id, 'ToDownload_stripe_refund_id: ' . $refund_id); } if ($amount !== null) { Orders::append_order_note($order_id, 'ToDownload_stripe_refund_amount: ' . (string) $amount . ' ' . $cur . ' (minor units)'); } wp_safe_redirect(add_query_arg([ 'ToDownload_notice' => 'refund_ok', ], $redirect_base)); exit; } $msg = (string) ($result['message'] ?? __('Stripe refund failed.', '2download-connector')); Orders::append_order_note($order_id, 'ToDownload_admin_action: refund via Stripe failed: ' . $msg); wp_safe_redirect(add_query_arg([ 'ToDownload_notice' => 'refund_failed', 'ToDownload_msg' => rawurlencode($msg), ], $redirect_base)); exit; } $msg = __('Stripe gateway not available.', '2download-connector'); Orders::append_order_note($order_id, 'ToDownload_admin_action: refund via Stripe failed: ' . $msg); wp_safe_redirect(add_query_arg([ 'ToDownload_notice' => 'refund_failed', 'ToDownload_msg' => rawurlencode($msg), ], $redirect_base)); exit; } Orders::update_status($order_id, 'refunded'); Orders::append_order_note($order_id, 'ToDownload_admin_action: refunded (manual)'); wp_safe_redirect($redirect_base); exit; } public function handle_save_settings(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('ToDownload_save_settings', 'ToDownload_settings_nonce'); $post = wp_unslash($_POST); $post_settings = isset($post['ToDownload_settings']) ? $post['ToDownload_settings'] : []; $value = is_array($post_settings) ? $post_settings : []; // Use the registered sanitization callback $callback = $this->get_settings_sanitize_callback(); $settings = $callback($value); // Connector mode: never store payment processor secrets on seller sites. $legacyStripeSecretKeyField = 'stripe' . '_secret' . '_key'; $legacyStripeWebhookSecretField = 'stripe' . '_webhook' . '_secret'; unset($settings[$legacyStripeSecretKeyField], $settings[$legacyStripeWebhookSecretField]); update_option('ToDownload_settings', $settings); $return_page = isset($_POST['ToDownload_return_page']) ? sanitize_key((string) wp_unslash($_POST['ToDownload_return_page'])) : ''; $allowed = [ 'ToDownload_settings' => true, 'ToDownload_connect' => true, ]; $page = isset($allowed[$return_page]) ? $return_page : 'ToDownload_settings'; wp_safe_redirect(admin_url('admin.php?page=' . $page . '&settings-updated=true')); exit; } private function get_settings_sanitize_callback(): callable { return function ($value) { $v = is_array($value) ? $value : []; $current = get_option('ToDownload_settings', []); if (!is_array($current)) { $current = []; } $out = []; // Hosted platform connection (2dl.app) $candidate_base = esc_url_raw((string) ($v['hosted_api_base_url'] ?? ($current['hosted_api_base_url'] ?? 'https://2dl.app'))); $candidate_base = trim((string) $candidate_base); $candidate_host = strtolower(sanitize_text_field((string) (wp_parse_url($candidate_base, PHP_URL_HOST) ?? ''))); $candidate_scheme = strtolower(sanitize_text_field((string) (wp_parse_url($candidate_base, PHP_URL_SCHEME) ?? ''))); // Safety: hosted API base must be http(s) and must NOT point at a WordPress host. // This prevents accidental values like https://staging.wp.2dl.app which cause 404s for /api/v1/*. $is_wp_host = ($candidate_host === 'wp.2dl.app' || ($candidate_host !== '' && str_ends_with($candidate_host, '.wp.2dl.app'))); $is_http = ($candidate_scheme === 'https' || $candidate_scheme === 'http'); if ($candidate_base === '' || $candidate_host === '' || !$is_http || $is_wp_host) { // If the user tried to set a staging-ish WP host, default to staging. Otherwise default to prod. $fallback = ($candidate_host !== '' && str_contains($candidate_host, 'staging')) ? 'https://staging.2dl.app' : 'https://2dl.app'; $candidate_base = $fallback; $candidate_host = strtolower((string) (wp_parse_url($candidate_base, PHP_URL_HOST) ?? '')); $candidate_scheme = 'https'; } // Normalize canonical bases. if ($candidate_host === 'www.2dl.app') { $candidate_base = 'https://2dl.app'; } else if ($candidate_host === '2dl.app') { $candidate_base = 'https://2dl.app'; } else if ($candidate_host === 'staging.2dl.app') { $candidate_base = 'https://staging.2dl.app'; } $out['hosted_api_base_url'] = rtrim($candidate_base, '/'); $out['hosted_store_public_id'] = sanitize_text_field((string) ($v['hosted_store_public_id'] ?? ($current['hosted_store_public_id'] ?? ''))); $incoming_hosted_key = trim(sanitize_text_field((string) ($v['hosted_store_api_key'] ?? ''))); if ($incoming_hosted_key === '') { $out['hosted_store_api_key'] = (string) ($current['hosted_store_api_key'] ?? ''); } else { $out['hosted_store_api_key'] = $incoming_hosted_key; } $out['platform_fee_percent'] = (float) ($v['platform_fee_percent'] ?? 3); if ($out['platform_fee_percent'] < 0) { $out['platform_fee_percent'] = 0.0; } if ($out['platform_fee_percent'] > 100) { $out['platform_fee_percent'] = 100.0; } $out['success_url'] = esc_url_raw((string) ($v['success_url'] ?? '')); $out['cancel_url'] = esc_url_raw((string) ($v['cancel_url'] ?? '')); $out['checkout_url'] = esc_url_raw((string) ($v['checkout_url'] ?? '')); $out['checkout_page_id'] = (int) ($v['checkout_page_id'] ?? 0); $out['account_page_id'] = (int) ($v['account_page_id'] ?? 0); $out['cart_page_id'] = (int) ($v['cart_page_id'] ?? 0); $out['products_page_id'] = (int) ($v['products_page_id'] ?? 0); $out['storefront_page_id'] = (int) ($v['storefront_page_id'] ?? 0); $out['enable_tax'] = !empty($v['enable_tax']) ? 1 : 0; $out['tax_percent'] = (float) ($v['tax_percent'] ?? 0); if ($out['tax_percent'] < 0) { $out['tax_percent'] = 0.0; } if ($out['tax_percent'] > 100) { $out['tax_percent'] = 100.0; } $out['default_access_days'] = (int) ($v['default_access_days'] ?? 0); if ($out['default_access_days'] < 0) { $out['default_access_days'] = 0; } // Seller tier pricing (Standard/Pro/Studio) $defaults = [ 'starter' => ['annual_fee_usd' => 0, 'platform_fee_percent' => 12.0], 'pro' => ['annual_fee_usd' => 149, 'platform_fee_percent' => 3.5], 'studio' => ['annual_fee_usd' => 299, 'platform_fee_percent' => 1.0], ]; foreach (['starter', 'pro', 'studio'] as $tier) { $fee_key = 'seller_tier_' . $tier . '_platform_fee_percent'; $annual_key = 'seller_tier_' . $tier . '_annual_fee_usd'; $fee_val = array_key_exists($fee_key, $v) ? (float) $v[$fee_key] : (array_key_exists($fee_key, $current) ? (float) $current[$fee_key] : (float) ($defaults[$tier]['platform_fee_percent'] ?? 0)); $out[$fee_key] = max(0.0, min(100.0, (float) $fee_val)); $annual_val = array_key_exists($annual_key, $v) ? (int) $v[$annual_key] : (array_key_exists($annual_key, $current) ? (int) $current[$annual_key] : (int) ($defaults[$tier]['annual_fee_usd'] ?? 0)); $out[$annual_key] = max(0, (int) $annual_val); } // Seller plan products (optional). If configured, sellers can purchase these to change subscription tier. $out['seller_plan_pro_product_id'] = max(0, (int) ($v['seller_plan_pro_product_id'] ?? ($current['seller_plan_pro_product_id'] ?? 0))); $out['seller_plan_studio_product_id'] = max(0, (int) ($v['seller_plan_studio_product_id'] ?? ($current['seller_plan_studio_product_id'] ?? 0))); // Best-effort: keep plan product prices synced to configured annual fees. // (Pricing is stored as post meta on the product.) try { $pro_price = (int) ($out['seller_tier_pro_annual_fee_usd'] ?? 0); $studio_price = (int) ($out['seller_tier_studio_annual_fee_usd'] ?? 0); if ($out['seller_plan_pro_product_id'] > 0 && $pro_price >= 0) { update_post_meta((int) $out['seller_plan_pro_product_id'], '_ToDownload_price', (string) number_format((float) $pro_price, 2, '.', '')); update_post_meta((int) $out['seller_plan_pro_product_id'], '_ToDownload_seller_membership_tier', 'pro'); } if ($out['seller_plan_studio_product_id'] > 0 && $studio_price >= 0) { update_post_meta((int) $out['seller_plan_studio_product_id'], '_ToDownload_price', (string) number_format((float) $studio_price, 2, '.', '')); update_post_meta((int) $out['seller_plan_studio_product_id'], '_ToDownload_seller_membership_tier', 'studio'); } } catch (\Throwable $e) { // Ignore (settings can still be saved). } return $out; }; } public function register_settings(): void { register_setting('ToDownload_settings_group', 'ToDownload_settings', [ 'type' => 'array', 'sanitize_callback' => $this->get_settings_sanitize_callback() ]); } public function render_orders(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $filters_ok = false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading nonce itself for verification. $filters_nonce = isset($_GET[self::ORDERS_FILTER_NONCE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::ORDERS_FILTER_NONCE_PARAM])) : ''; if ($filters_nonce !== '' && wp_verify_nonce($filters_nonce, self::ORDERS_FILTER_NONCE_ACTION)) { $filters_ok = true; } // Read-only GET parameters used for notices/filters. $notice = ($filters_ok && isset($_GET['ToDownload_notice'])) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_notice'])) : ''; $msg = ($filters_ok && isset($_GET['ToDownload_msg'])) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_msg'])) : ''; $orders_notice = ($filters_ok && isset($_GET['ToDownload_orders_notice'])) ? sanitize_key((string) wp_unslash($_GET['ToDownload_orders_notice'])) : ''; if ($notice === 'refund_failed') { echo '

' . esc_html($msg !== '' ? rawurldecode($msg) : __('Refund failed.', '2download-connector')) . '

'; } else if ($notice === 'refund_ok') { echo '

' . esc_html__('Refunded via Stripe.', '2download-connector') . '

'; } if ($orders_notice === 'default_saved') { echo '

' . esc_html__('Default seller filter saved.', '2download-connector') . '

'; } else if ($orders_notice === 'default_cleared') { echo '

' . esc_html__('Default seller filter cleared.', '2download-connector') . '

'; } echo '

' . esc_html__('Orders', '2download-connector') . '

'; $q = ($filters_ok && isset($_GET['ToDownload_q'])) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_q'])) : ''; $status_filter = ($filters_ok && isset($_GET['ToDownload_status'])) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_status'])) : ''; $view_order_id = ($filters_ok && isset($_GET['ToDownload_view_order_id'])) ? absint(wp_unslash($_GET['ToDownload_view_order_id'])) : 0; $seller_filter_param_present = ($filters_ok && isset($_GET['ToDownload_seller_user_id'])); $seller_filter_param = $seller_filter_param_present ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_seller_user_id'])) : ''; $seller_user_id = 0; if ($seller_filter_param_present) { $seller_user_id = max(0, (int) $seller_filter_param); } else { $default_seller_user_id = (int) get_user_meta((int) get_current_user_id(), 'ToDownload_default_orders_seller_user_id', true); $seller_user_id = max(0, $default_seller_user_id); } $stats = Orders::stats_summary(30); $last_orders = (int) ($stats['last_30_orders'] ?? 0); $last_total = (float) ($stats['last_30_total'] ?? 0); $all_orders = (int) ($stats['all_orders'] ?? 0); $all_total = (float) ($stats['all_total'] ?? 0); echo '

' . esc_html__('Completed (last 30 days):', '2download-connector') . ' ' . esc_html((string) $last_orders) . ' ' . esc_html__('orders', '2download-connector') . ' / ' . esc_html(Money::usd((float) $last_total)) . '   ' . esc_html__('Completed (all time):', '2download-connector') . ' ' . esc_html((string) $all_orders) . ' ' . esc_html__('orders', '2download-connector') . ' / ' . esc_html(Money::usd((float) $all_total)) . '

'; $export_url = add_query_arg([ 'ToDownload_export_orders' => 1, 'ToDownload_q' => $q, 'ToDownload_status' => $status_filter, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php?page=ToDownload_admin')); $export_url = wp_nonce_url($export_url, 'ToDownload_export_orders', 'ToDownload_nonce'); $set_default_url = ''; if ($seller_user_id > 0) { $set_default_url = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_orders_set_default_seller' => 1, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php')); $set_default_url = wp_nonce_url($set_default_url, 'ToDownload_orders_set_default_seller', 'ToDownload_nonce'); } $clear_default_url = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_orders_clear_default_seller' => 1, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php')); $clear_default_url = wp_nonce_url($clear_default_url, 'ToDownload_orders_clear_default_seller', 'ToDownload_nonce'); $sellers = get_users([ 'role' => 'todownload_seller', 'orderby' => 'display_name', 'order' => 'ASC', 'fields' => ['ID', 'display_name', 'user_email'], 'number' => 2000, ]); if (!is_array($sellers)) { $sellers = []; } // Order details view (separate screen, no modal). if ($view_order_id > 0) { $order = Orders::get_order($view_order_id); if (!$order) { echo '

' . esc_html__('Order not found.', '2download-connector') . '

'; $back = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_q' => $q, 'ToDownload_status' => $status_filter, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php')); echo '

' . esc_html__('← Back to Orders', '2download-connector') . '

'; echo '
'; return; } $items = Orders::get_order_items($view_order_id); $customer_id = (int) ($order['customer_id'] ?? 0); $customer = $customer_id > 0 ? Orders::get_customer($customer_id) : null; $customer_email = $customer && isset($customer['email']) ? (string) $customer['email'] : ''; $customer_first = $customer && isset($customer['first_name']) ? trim((string) $customer['first_name']) : ''; $customer_last = $customer && isset($customer['last_name']) ? trim((string) $customer['last_name']) : ''; $customer_name = trim($customer_first . ' ' . $customer_last); $customer_user_id = $customer && isset($customer['user_id']) ? (int) $customer['user_id'] : 0; $back = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_q' => $q, 'ToDownload_status' => $status_filter, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php')); echo '

' . esc_html__('← Back to Orders', '2download-connector') . '

'; /* translators: %d = order ID number */ echo '

' . sprintf(esc_html__('Order #%d', '2download-connector'), (int) $view_order_id) . '

'; echo '

' . esc_html__('Summary', '2download-connector') . '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $seller_id = (int) ($order['seller_user_id'] ?? 0); if ($seller_id > 0) { $seller_u = get_user_by('id', $seller_id); $seller_label = $seller_u instanceof \WP_User ? trim((string) ($seller_u->display_name ?? '')) : ''; $seller_email = $seller_u instanceof \WP_User ? trim((string) ($seller_u->user_email ?? '')) : ''; if ($seller_label === '') { $seller_label = 'User #' . (string) $seller_id; } if ($seller_email !== '') { $seller_label .= ' (' . $seller_email . ')'; } $seller_edit = admin_url('user-edit.php?user_id=' . (int) $seller_id); echo ''; } echo '
' . esc_html__('Status', '2download-connector') . '' . esc_html((string) ($order['status'] ?? '')) . '
' . esc_html__('Created', '2download-connector') . '' . esc_html((string) ($order['created_at'] ?? '')) . '
' . esc_html__('Updated', '2download-connector') . '' . esc_html((string) ($order['updated_at'] ?? '')) . '
' . esc_html__('Total (USD)', '2download-connector') . '' . esc_html(Money::usd((float) ($order['total'] ?? 0))) . '
' . esc_html__('Gateway', '2download-connector') . '' . esc_html((string) ($order['gateway'] ?? '')) . '
' . esc_html__('Transaction', '2download-connector') . '' . esc_html((string) (($order['transaction_id'] ?? '') !== '' ? $order['transaction_id'] : '—')) . '
' . esc_html__('Seller', '2download-connector') . '' . esc_html($seller_label) . '
'; echo '

' . esc_html__('Customer', '2download-connector') . '

'; echo ''; echo ''; echo ''; if ($customer_email !== '') { echo ''; } if ($customer_user_id > 0) { $customer_user_edit = admin_url('user-edit.php?user_id=' . (int) $customer_user_id); echo ''; } echo '
' . esc_html__('Name', '2download-connector') . '' . esc_html($customer_name !== '' ? $customer_name : __('(no name)', '2download-connector')) . '
' . esc_html__('Email', '2download-connector') . '' . esc_html($customer_email) . '
' . esc_html__('WP User', '2download-connector') . '' . esc_html('User #' . (string) $customer_user_id) . '
'; echo '

' . esc_html__('Items & downloads', '2download-connector') . '

'; if (empty($items)) { echo '

' . esc_html__('No items found for this order.', '2download-connector') . '

'; } else { echo ''; echo '' . '' . '' . '' . '' . '' . ''; $all_link = \ToDownload\Fulfillment\DownloadDelivery::generate_download_all_link((int) $view_order_id); foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); $title = $download_id > 0 ? (string) get_the_title($download_id) : ''; $qty = (int) ($it['qty'] ?? 0); $line_total = (float) ($it['line_total'] ?? 0); $link = $download_id > 0 ? \ToDownload\Fulfillment\DownloadDelivery::generate_link((int) $view_order_id, (int) $download_id) : ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Product', '2download-connector') . '' . esc_html__('Title', '2download-connector') . '' . esc_html__('Qty', '2download-connector') . '' . esc_html__('Line total', '2download-connector') . '' . esc_html__('Download link', '2download-connector') . '
' . esc_html($download_id > 0 ? (string) $download_id : '—') . '' . esc_html($title !== '' ? $title : '—') . '' . esc_html((string) max(1, $qty)) . '' . esc_html(Money::usd((float) $line_total)) . ''; if ($link !== '') { echo '' . esc_html__('Download', '2download-connector') . ' '; } echo '' . esc_html__('Download all', '2download-connector') . ''; echo '
'; } $notes = (string) ($order['notes'] ?? ''); echo '

' . esc_html__('Order notes', '2download-connector') . '

'; if (trim($notes) === '') { echo '

' . esc_html__('No notes.', '2download-connector') . '

'; } else { echo '
' . esc_html($notes) . '
'; } echo '

' . esc_html__('Webhook history', '2download-connector') . '

'; $logs = WebhookLog::all(200); $hits = []; foreach ($logs as $r) { if (!is_array($r)) { continue; } if ((int) ($r['order_id'] ?? 0) === (int) $view_order_id) { $hits[] = $r; } } $webhooks_url = add_query_arg([ 'page' => 'ToDownload_webhooks', 'ToDownload_order_id' => (int) $view_order_id, ], admin_url('admin.php')); echo '

' . esc_html__('Open Webhooks log', '2download-connector') . '

'; if (empty($hits)) { echo '

' . esc_html__('No webhook log entries for this order.', '2download-connector') . '

'; } else { echo ''; echo '' . '' . '' . '' . '' . ''; foreach ($hits as $r) { $ts = (string) ($r['ts'] ?? ''); $gw = (string) ($r['gateway'] ?? ''); $event = (string) ($r['event_id'] ?? ''); $msg2 = (string) ($r['message'] ?? ''); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Time', '2download-connector') . '' . esc_html__('Gateway', '2download-connector') . '' . esc_html__('Event', '2download-connector') . '' . esc_html__('Message', '2download-connector') . '
' . esc_html($ts !== '' ? $ts : '—') . '' . esc_html($gw !== '' ? $gw : '—') . '' . ($event !== '' ? '' . esc_html($event) . '' : '—') . '' . esc_html($msg2 !== '' ? $msg2 : '—') . '
'; } echo ''; return; } echo '
'; echo ''; echo ''; echo ' '; echo ' '; echo ' '; submit_button(__('Filter', '2download-connector'), 'secondary', '', false); echo ' ' . esc_html__('Export CSV', '2download-connector') . ''; if ($set_default_url !== '') { echo ' ' . esc_html__('Make default', '2download-connector') . ''; } echo ' ' . esc_html__('Clear default', '2download-connector') . ''; echo '
'; $rows = Orders::list_orders_filtered(50, 0, [ 'q' => $q, 'status' => $status_filter, 'seller_user_id' => $seller_user_id, ]); if (empty($rows)) { echo '

' . esc_html__('No orders yet.', '2download-connector') . '

'; echo ''; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $seller_labels = []; $confirm_complete = __('Mark this order as complete?', '2download-connector'); $confirm_refund = __('Refund this order? This action may be irreversible.', '2download-connector'); foreach ($rows as $o) { $order_id = (int) ($o['id'] ?? 0); $status = (string) ($o['status'] ?? ''); $date = (string) ($o['created_at'] ?? ''); $email = (string) ($o['customer_email'] ?? ''); $first = trim((string) ($o['customer_first_name'] ?? '')); $last = trim((string) ($o['customer_last_name'] ?? '')); $name = trim($first . ' ' . $last); $items_count = (int) ($o['items_count'] ?? 0); $total = (float) ($o['total'] ?? 0); $gateway = (string) ($o['gateway'] ?? ''); $txn = (string) ($o['transaction_id'] ?? ''); $seller_user_id = (int) ($o['seller_user_id'] ?? 0); $refund_label = Orders::refund_status_label($o); $seller_cell = '—'; if ($seller_user_id > 0) { if (!array_key_exists($seller_user_id, $seller_labels)) { $u = get_user_by('id', $seller_user_id); $edit_url = admin_url('user-edit.php?user_id=' . (int) $seller_user_id); if ($u instanceof \WP_User) { $name_label = trim((string) ($u->display_name ?? '')); $email_label = trim((string) ($u->user_email ?? '')); if ($name_label === '') { $name_label = 'User #' . (string) $seller_user_id; } $seller_labels[$seller_user_id] = [ 'label' => $name_label . ($email_label !== '' ? ' (' . $email_label . ')' : ''), 'edit' => $edit_url, ]; } else { $seller_labels[$seller_user_id] = [ 'label' => 'User #' . (string) $seller_user_id, 'edit' => $edit_url, ]; } } $seller_cell = '' . esc_html((string) ($seller_labels[$seller_user_id]['label'] ?? ('User #' . (string) $seller_user_id))) . ''; } $base = admin_url('admin.php?page=ToDownload_admin'); $webhooks_url = add_query_arg([ 'page' => 'ToDownload_webhooks', 'ToDownload_order_id' => $order_id, ], admin_url('admin.php')); $return_url = add_query_arg([ 'ToDownload_q' => $q, 'ToDownload_status' => $status_filter, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], $base); $action_post_url = admin_url('admin-post.php'); $nonce_value = wp_create_nonce('ToDownload_order_action_' . $order_id); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Order', '2download-connector') . '' . esc_html__('Date', '2download-connector') . '' . esc_html__('Status', '2download-connector') . '' . esc_html__('Customer', '2download-connector') . '' . esc_html__('Seller', '2download-connector') . '' . esc_html__('Items', '2download-connector') . '' . esc_html__('Total', '2download-connector') . '' . esc_html__('Gateway', '2download-connector') . '' . esc_html__('Transaction', '2download-connector') . '' . esc_html__('Refund', '2download-connector') . '' . esc_html__('Actions', '2download-connector') . '
#' . esc_html((string) $order_id) . '' . esc_html($date) . '' . esc_html($status) . '' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . '
' . esc_html($email) . '
' . wp_kses_post($seller_cell) . '' . esc_html((string) $items_count) . '' . esc_html(Money::usd((float) $total)) . '' . esc_html($gateway !== '' ? $gateway : '—') . '' . esc_html($txn !== '' ? $txn : '—') . '' . esc_html($refund_label) . ''; $view_url = add_query_arg([ 'page' => 'ToDownload_admin', 'ToDownload_view_order_id' => $order_id, 'ToDownload_q' => $q, 'ToDownload_status' => $status_filter, 'ToDownload_seller_user_id' => (int) $seller_user_id, self::ORDERS_FILTER_NONCE_PARAM => wp_create_nonce(self::ORDERS_FILTER_NONCE_ACTION), ], admin_url('admin.php')); echo '' . esc_html__('View', '2download-connector') . ' '; if ($status !== 'complete') { echo '
' . '' . '' . '' . '' . '' . '' . '
'; } if ($status !== 'refunded') { echo '
' . '' . '' . '' . '' . '' . '' . '
'; } echo ' ' . esc_html__('Webhooks', '2download-connector') . ''; echo '
'; echo '

' . esc_html__('Refunds: Stripe orders can be refunded via Stripe; other gateways are manual.', '2download-connector') . '

'; echo ''; } public function render_customers(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } echo '

' . esc_html__('Customers', '2download-connector') . '

'; $rows = Orders::list_customers(50, 0); if (empty($rows)) { echo '

' . esc_html__('No customers yet.', '2download-connector') . '

'; echo '
'; return; } echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($rows as $c) { $first = trim((string) ($c['first_name'] ?? '')); $last = trim((string) ($c['last_name'] ?? '')); $name = trim($first . ' ' . $last); $email = (string) ($c['email'] ?? ''); $orders_count = (int) ($c['orders_count'] ?? 0); $total = (float) ($c['total_spent'] ?? 0); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Customer', '2download-connector') . '' . esc_html__('Email', '2download-connector') . '' . esc_html__('Orders', '2download-connector') . '' . esc_html__('Total Spent', '2download-connector') . '
' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . '' . esc_html($email) . '' . esc_html((string) $orders_count) . '' . esc_html(Money::usd((float) $total)) . '
'; echo ''; } public function render_settings(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin notices populated by internal redirects; no state changes. $test_notice = isset($_GET['ToDownload_admin_test_plan_notice']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_test_plan_notice'])) : ''; $test_error = isset($_GET['ToDownload_admin_test_plan_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_test_plan_error'])) : ''; $test_seller_user_id = isset($_GET['seller_user_id']) ? absint(wp_unslash($_GET['seller_user_id'])) : 0; $grant_notice = isset($_GET['ToDownload_admin_grant_notice']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_grant_notice'])) : ''; $grant_error = isset($_GET['ToDownload_admin_grant_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_grant_error'])) : ''; $plan_discount_notice = isset($_GET['ToDownload_admin_plan_discount_notice']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_plan_discount_notice'])) : ''; $plan_discount_error = isset($_GET['ToDownload_admin_plan_discount_error']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_admin_plan_discount_error'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended ?>

Privacy Policy, Terms, Refunds.', '2download-connector'), 'https://2dl.app/privacy', 'https://2dl.app/terms', 'https://2dl.app/refunds' ), [ 'a' => [ 'href' => true, 'target' => true, 'rel' => true, ], ] ); ?>

ops_tools_enabled()) : ?> ops_tools_enabled()) : ?>

'stripe_checkout'], home_url('/'))); ?>