BuyerErrors::cart_url(), 'label' => __('Back to cart', '2download-connector'), 'primary' => true], ['href' => home_url('/'), 'label' => __('Continue shopping', '2download-connector')], ], 400 ); } // Nonce verified above; remaining params are read-only for this action. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $price_id = isset($_GET['ToDownload_price_id']) ? absint(wp_unslash($_GET['ToDownload_price_id'])) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $buy_now = isset($_GET['ToDownload_buy_now']) && sanitize_key((string) wp_unslash($_GET['ToDownload_buy_now'])) === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $clear_cart = isset($_GET['ToDownload_clear_cart']) && sanitize_key((string) wp_unslash($_GET['ToDownload_clear_cart'])) === '1'; if ($buy_now && $clear_cart) { Cart::instance()->clear(); } Cart::instance()->add($download_id, ['price_id' => $price_id]); if ($buy_now) { $settings = get_option('ToDownload_settings', []); $checkout_page_id = is_array($settings) ? (int) ($settings['checkout_page_id'] ?? 0) : 0; $checkout_url = is_array($settings) ? (string) ($settings['checkout_url'] ?? '') : ''; $target = $checkout_page_id ? get_permalink($checkout_page_id) : ''; if (!$target && $checkout_url) { $target = $checkout_url; } if (!$target) { $target = home_url('/'); } // Optional prefill for checkout (useful for renew links from account). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $prefill_email = isset($_GET['ToDownload_email']) ? sanitize_email((string) wp_unslash($_GET['ToDownload_email'])) : ''; if ($prefill_email === '' && is_user_logged_in()) { $user = wp_get_current_user(); $prefill_email = $user ? (string) $user->user_email : ''; } if ($prefill_email !== '' && is_email($prefill_email)) { $target = add_query_arg(['ToDownload_email' => $prefill_email], $target); } wp_safe_redirect($target); exit; } wp_safe_redirect(remove_query_arg(['ToDownload_add_to_cart', 'ToDownload_nonce', 'ToDownload_price_id', 'ToDownload_buy_now', 'ToDownload_clear_cart'])); exit; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified inside this block before any state change. if (isset($_GET['ToDownload_remove_from_cart'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified immediately below. $download_id = absint(wp_unslash($_GET['ToDownload_remove_from_cart'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the nonce itself for verification. $nonce = isset($_GET['ToDownload_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_nonce'])) : ''; if (!$download_id || !wp_verify_nonce($nonce, 'ToDownload_remove_from_cart_' . $download_id)) { BuyerErrors::render_panel( __('Invalid request', '2download-connector'), __('We couldn’t remove that item. Please try again.', '2download-connector'), [ ['href' => BuyerErrors::cart_url(), 'label' => __('Back to cart', '2download-connector'), 'primary' => true], ['href' => home_url('/'), 'label' => __('Continue shopping', '2download-connector')], ], 400 ); } Cart::instance()->remove($download_id); wp_safe_redirect(remove_query_arg(['ToDownload_remove_from_cart', 'ToDownload_nonce'])); exit; } // Checkout submission $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper(sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_METHOD']))) : ''; if ($method === 'POST' && isset($_POST['ToDownload_checkout_nonce'])) { $this->process_checkout(); } } private function process_checkout(): void { $nonce = isset($_POST['ToDownload_checkout_nonce']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_checkout_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'ToDownload_checkout')) { $this->fail_and_redirect([ __('Invalid request. Please try again.', '2download-connector'), ], []); } $post_raw = wp_unslash($_POST); $post = $this->sanitize_checkout_post(is_array($post_raw) ? $post_raw : []); $cart = Cart::instance(); $items = $cart->all(); if (empty($items)) { $this->fail_and_redirect([ __('Your cart is empty.', '2download-connector'), ], []); } $email = sanitize_email((string) ($post['ToDownload_email'] ?? '')); $first = sanitize_text_field((string) ($post['ToDownload_first_name'] ?? '')); $last = sanitize_text_field((string) ($post['ToDownload_last_name'] ?? '')); $gateway_id = sanitize_text_field((string) ($post['ToDownload_gateway'] ?? 'hosted_checkout')); $discount_code = strtoupper(trim(sanitize_text_field((string) ($post['ToDownload_discount_code'] ?? '')))); $errors = []; $membership_tier = $this->membership_tier_from_cart_items($items); $seller_user_id = 0; if ($membership_tier === '') { $seller_user_id = $this->resolve_seller_user_id_from_cart_items($items); if ($seller_user_id <= 0) { $errors[] = __('Your cart must contain products from exactly one seller.', '2download-connector'); } } else { // Seller membership purchases are platform orders (not routed to a seller account). if (!is_user_logged_in()) { $errors[] = __('Please log in to purchase a seller plan.', '2download-connector'); } else if (!current_user_can('edit_todownload_downloads')) { $errors[] = __('Only sellers can purchase a seller plan.', '2download-connector'); } } if (!$email || !is_email($email)) { $errors[] = __('Please enter a valid email.', '2download-connector'); } if (trim($first) === '') { $errors[] = __('Please enter your first name.', '2download-connector'); } if (trim($last) === '') { $errors[] = __('Please enter your last name.', '2download-connector'); } $discount_result = [ 'ok' => true, 'discount' => 0.0, 'message' => '', ]; if ($discount_code !== '') { $discount_result = Discounts::apply_discount_code($discount_code, $items, $cart->subtotal(), $seller_user_id, $email); if (!($discount_result['ok'] ?? false)) { $errors[] = (string) ($discount_result['message'] ?? __('Invalid discount code.', '2download-connector')); } } $subtotal = $cart->subtotal(); $tax = $cart->tax(); $discount = (float) ($discount_result['discount'] ?? $cart->discount()); $total = max(0.0, $subtotal + $tax - $discount); $gateway = null; if ($total > 0) { $gateway = GatewayRegistry::instance()->get($gateway_id); if (!$gateway) { $errors[] = __('Selected gateway is not available.', '2download-connector'); } } // Connector-only: payment onboarding is handled on 2dl.app. // Pass a sanitized view of the checkout payload to filter consumers. // (Prevents untrusted input from being forwarded without sanitization.) $errors = (array) apply_filters('ToDownload_checkout_validate', $errors, $post, $items); if (!empty($errors)) { $this->fail_and_redirect($errors, [ 'email' => $email, 'first_name' => $first, 'last_name' => $last, 'discount_code' => $discount_code, 'gateway_id' => $gateway_id, ]); } do_action('ToDownload_before_create_order', $email, $items); $customer_id = Orders::create_customer([ 'user_id' => is_user_logged_in() ? get_current_user_id() : null, 'email' => $email, 'first_name' => $first, 'last_name' => $last, ]); $order_id = Orders::create_order([ 'customer_id' => $customer_id, // For membership purchases, this is intentionally 0 (stored as NULL) so payments stay on the platform. 'seller_user_id' => $membership_tier === '' ? $seller_user_id : 0, 'status' => 'pending', 'subtotal' => $subtotal, 'tax' => $tax, 'discount' => $discount, 'total' => $total, 'gateway' => $total > 0 ? $gateway_id : 'free', ]); if ($discount_code !== '' && !empty($discount_result['ok'])) { Orders::append_order_note($order_id, 'ToDownload_discount_code: ' . $discount_code); Orders::append_order_note($order_id, 'ToDownload_discount_amount: ' . number_format($discount, 2)); } foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if (!$download_id) { continue; } $price = isset($it['price']) ? (float) $it['price'] : (float) get_post_meta($download_id, '_ToDownload_price', true); $product_type = (string) get_post_meta($download_id, '_ToDownload_product_type', true); if ($product_type === 'bundle') { $bundle_items = get_post_meta($download_id, '_ToDownload_bundle_items', true); if (!is_array($bundle_items)) { $bundle_items = []; } $bundle_items = array_values(array_filter(array_map('intval', $bundle_items))); if (!empty($bundle_items)) { Orders::append_order_note($order_id, 'ToDownload_bundle_price: ' . $download_id . ':' . number_format((float) $price, 2, '.', '')); Orders::append_order_note($order_id, 'ToDownload_bundle: ' . $download_id . ' -> ' . implode(',', $bundle_items)); foreach ($bundle_items as $child_id) { Orders::add_order_item($order_id, (int) $child_id, 0.0); } continue; } } Orders::add_order_item($order_id, $download_id, $price); } do_action('ToDownload_order_created', $order_id); if ($total <= 0) { $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $success = (string) ($settings['success_url'] ?? home_url('/')); if ($success === '') { $success = home_url('/'); } $order_public_id = Orders::order_public_id((int) $order_id); $success = add_query_arg([ 'ToDownload_order' => $order_public_id, 'ToDownload_receipt_token' => Token::receipt_token($order_public_id, (string) $email), ], $success); Orders::update_status($order_id, 'complete', 'free_' . (int) $order_id); do_action('ToDownload_after_order_complete', $order_id); $cart->clear(); wp_safe_redirect($success); exit; } $redirect = $gateway->start_payment($order_id, [ 'email' => $email, ]); $cart->clear(); $redirect = (string) $redirect; $target_host = (string) (wp_parse_url($redirect, PHP_URL_HOST) ?? ''); $site_host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); if ($target_host !== '' && $site_host !== '' && strcasecmp($target_host, $site_host) !== 0) { add_filter('allowed_redirect_hosts', function($hosts) use ($target_host) { $hosts[] = $target_host; return $hosts; }); } wp_safe_redirect($redirect); exit; } private function fail_and_redirect(array $errors, array $prefill): void { $target = wp_get_referer(); if (!$target) { $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '/'; $target = home_url(add_query_arg([], $request_uri)); } $msg = trim(implode("\n", array_filter(array_map('strval', $errors)))); if ($msg === '') { $msg = (string) __('Please check your details and try again.', '2download-connector'); } $args = [ 'ToDownload_checkout_error' => $msg, ]; $email = isset($prefill['email']) ? sanitize_email((string) $prefill['email']) : ''; if ($email !== '' && is_email($email)) { $args['ToDownload_email'] = $email; } $first = isset($prefill['first_name']) ? sanitize_text_field((string) $prefill['first_name']) : ''; $last = isset($prefill['last_name']) ? sanitize_text_field((string) $prefill['last_name']) : ''; if ($first !== '') { $args['ToDownload_first_name'] = $first; } if ($last !== '') { $args['ToDownload_last_name'] = $last; } $code = isset($prefill['discount_code']) ? sanitize_text_field((string) $prefill['discount_code']) : ''; if ($code !== '') { $args['ToDownload_discount_code'] = strtoupper($code); } $gateway = isset($prefill['gateway_id']) ? sanitize_text_field((string) $prefill['gateway_id']) : ''; if ($gateway !== '') { $args['ToDownload_gateway'] = $gateway; } wp_safe_redirect(add_query_arg($args, $target)); exit; } private function resolve_seller_user_id_from_cart_items(array $items): int { $author_ids = []; foreach ($items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if ($download_id <= 0) { continue; } $download_ids = [$download_id]; $product_type = (string) get_post_meta($download_id, '_ToDownload_product_type', true); if ($product_type === 'bundle') { $bundle_items = get_post_meta($download_id, '_ToDownload_bundle_items', true); if (!is_array($bundle_items)) { $bundle_items = []; } $bundle_items = array_values(array_filter(array_map('intval', $bundle_items))); if (!empty($bundle_items)) { $download_ids = array_values(array_unique(array_merge($download_ids, $bundle_items))); } } foreach ($download_ids as $id) { $id = (int) $id; if ($id <= 0) { continue; } if ((string) get_post_type($id) !== DownloadPostType::POST_TYPE) { continue; } $author = (int) get_post_field('post_author', $id); if ($author > 0) { $author_ids[$author] = true; } } } $ids = array_keys($author_ids); if (count($ids) !== 1) { return 0; } return (int) $ids[0]; } /** * Returns the membership tier when the cart is a seller plan purchase. * A valid membership purchase is exactly one item marked as Pro or Studio. */ private function membership_tier_from_cart_items(array $items): string { if (count($items) !== 1) { return ''; } $it = $items[0] ?? null; if (!is_array($it)) { return ''; } $download_id = (int) ($it['download_id'] ?? 0); if ($download_id <= 0) { return ''; } // Primary: per-product marker. $tier = sanitize_key((string) get_post_meta($download_id, self::META_SELLER_MEMBERSHIP_TIER, true)); if ($tier === Pricing::TIER_PRO || $tier === Pricing::TIER_STUDIO) { return $tier; } // Fallback: configured plan product IDs. $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $pro_id = (int) ($settings['seller_plan_pro_product_id'] ?? 0); $studio_id = (int) ($settings['seller_plan_studio_product_id'] ?? 0); if ($studio_id > 0 && $download_id === $studio_id) { return Pricing::TIER_STUDIO; } if ($pro_id > 0 && $download_id === $pro_id) { return Pricing::TIER_PRO; } return ''; } private function sanitize_checkout_post(array $post): array { $out = []; foreach ($post as $key => $value) { if (!is_string($key) || $key === '') { continue; } $out[$key] = $this->sanitize_checkout_value($key, $value); } return $out; } private function sanitize_checkout_value(string $key, $value) { if (is_array($value)) { $out = []; foreach ($value as $k => $v) { $child_key = is_string($k) ? $k : $key; $out[$k] = $this->sanitize_checkout_value($child_key, $v); } return $out; } if (is_bool($value)) { return $value; } if (is_int($value)) { return $value; } if (is_float($value)) { return $value; } $str = is_scalar($value) ? (string) $value : ''; if (stripos($key, 'nonce') !== false) { return sanitize_text_field($str); } if ($key === 'ToDownload_gateway') { return sanitize_key($str); } if ($key === 'ToDownload_email' || stripos($key, 'email') !== false) { return sanitize_email($str); } if (preg_match('/(^|_)(id|download_id|price_id)$/i', $key)) { return absint($str); } return sanitize_text_field($str); } }