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 '
' . 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') . '
' . esc_html__('Not connected yet. Enter your Store public ID and Store API key.', '2download-connector') . '
' . esc_html__('Advanced settings', '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 '| ' . esc_html__('Seller', '2download-connector') . ' | '; echo '' . esc_html__('Tier', '2download-connector') . ' | '; echo '' . esc_html__('Expires', '2download-connector') . ' | '; echo '' . esc_html__('Platform fee', '2download-connector') . ' | '; echo '' . esc_html__('Stripe fees', '2download-connector') . ' | '; echo '
|---|---|---|---|---|
| ' . esc_html($seller_label) . ' | '; echo '' . esc_html($this->tier_label($tier)) . ' | '; echo '' . esc_html($expires !== '' ? $expires : '—') . ' | '; echo '' . esc_html(rtrim(rtrim(number_format($platform_fee, 2, '.', ''), '0'), '.')) . '% | '; echo '' . esc_html($seller_pays_stripe ? __('Seller-paid', '2download-connector') : __('Included', '2download-connector')) . ' | '; echo '
' . esc_html__('Webhook logs cleared.', '2download-connector') . '
' . 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 ''; $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 '| ' . esc_html__('Time', '2download-connector') . ' | '; echo '' . esc_html__('Gateway', '2download-connector') . ' | '; echo '' . esc_html__('Type', '2download-connector') . ' | '; echo '' . esc_html__('Order', '2download-connector') . ' | '; echo '' . esc_html__('Event', '2download-connector') . ' | '; echo '' . esc_html__('Message', '2download-connector') . ' | '; echo '
|---|---|---|---|---|---|
| ' . esc_html($ts !== '' ? $ts : '—') . ' | '; echo '' . esc_html($gateway !== '' ? $gateway : '—') . ' | '; echo '' . esc_html($type !== '' ? $type : '—') . ' | '; echo '' . wp_kses_post($order_cell) . ' | '; echo '' . wp_kses_post($event_cell) . ' | '; echo '' . wp_kses_post($message_cell) . ' | '; echo '
' . esc_html($msg !== '' ? rawurldecode($msg) : __('Refund failed.', '2download-connector')) . '
' . esc_html__('Refunded via Stripe.', '2download-connector') . '
' . esc_html__('Default seller filter saved.', '2download-connector') . '
' . esc_html__('Default seller filter cleared.', '2download-connector') . '
' . 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') . '
' . esc_html__('← Back to Orders', '2download-connector') . '
'; echo '' . esc_html__('← Back to Orders', '2download-connector') . '
'; /* translators: %d = order ID number */ 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) . ' |
| ' . 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) . ' |
' . esc_html__('No items found for this order.', '2download-connector') . '
'; } else { 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 : '—') . ' | '; echo '' . esc_html($title !== '' ? $title : '—') . ' | '; echo '' . esc_html((string) max(1, $qty)) . ' | '; echo '' . esc_html(Money::usd((float) $line_total)) . ' | '; echo ''; if ($link !== '') { echo '' . esc_html__('Download', '2download-connector') . ' '; } echo '' . esc_html__('Download all', '2download-connector') . ''; echo ' | '; echo '
' . esc_html__('No notes.', '2download-connector') . '
'; } else { echo '' . esc_html($notes) . ''; } 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 '| ' . esc_html__('Time', '2download-connector') . ' | ' . '' . esc_html__('Gateway', '2download-connector') . ' | ' . '' . esc_html__('Event', '2download-connector') . ' | ' . '' . esc_html__('Message', '2download-connector') . ' | ' . '
|---|---|---|---|
| ' . esc_html($ts !== '' ? $ts : '—') . ' | '; echo '' . esc_html($gw !== '' ? $gw : '—') . ' | '; echo '' . ($event !== '' ? '' . esc_html($event) . '' : '—') . ' | ';
echo '' . esc_html($msg2 !== '' ? $msg2 : '—') . ' | '; echo '
' . esc_html__('No orders yet.', '2download-connector') . '
'; echo ''; return; } echo '| ' . esc_html__('Order', '2download-connector') . ' | '; echo '' . esc_html__('Date', '2download-connector') . ' | '; echo '' . esc_html__('Status', '2download-connector') . ' | '; echo '' . esc_html__('Customer', '2download-connector') . ' | '; echo '' . esc_html__('Seller', '2download-connector') . ' | '; echo '' . esc_html__('Items', '2download-connector') . ' | '; echo '' . esc_html__('Total', '2download-connector') . ' | '; echo '' . esc_html__('Gateway', '2download-connector') . ' | '; echo '' . esc_html__('Transaction', '2download-connector') . ' | '; echo '' . esc_html__('Refund', '2download-connector') . ' | '; echo '' . esc_html__('Actions', '2download-connector') . ' | '; echo '
|---|---|---|---|---|---|---|---|---|---|---|
| #' . esc_html((string) $order_id) . ' | '; echo '' . esc_html($date) . ' | '; echo '' . esc_html($status) . ' | '; echo '' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . ' ' . esc_html($email) . ' | ';
echo '' . wp_kses_post($seller_cell) . ' | '; echo '' . esc_html((string) $items_count) . ' | '; echo '' . esc_html(Money::usd((float) $total)) . ' | '; echo '' . esc_html($gateway !== '' ? $gateway : '—') . ' | '; echo '' . esc_html($txn !== '' ? $txn : '—') . ' | '; echo '' . esc_html($refund_label) . ' | '; echo ''; $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__('No customers yet.', '2download-connector') . '
'; echo '| ' . esc_html__('Customer', '2download-connector') . ' | '; echo '' . esc_html__('Email', '2download-connector') . ' | '; echo '' . esc_html__('Orders', '2download-connector') . ' | '; echo '' . esc_html__('Total Spent', '2download-connector') . ' | '; echo '
|---|---|---|---|
| ' . esc_html($name !== '' ? $name : __('(no name)', '2download-connector')) . ' | '; echo '' . esc_html($email) . ' | '; echo '' . esc_html((string) $orders_count) . ' | '; echo '' . esc_html(Money::usd((float) $total)) . ' | '; echo '
store_public_id, product_public_id, success_url, cancel_url
session_id
Authorization: Bearer ..., X-Request-Id
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, ], ] ); ?>
'stripe_checkout'], home_url('/'))); ?>