register_activation(); // WP 6.7+ warns if translations are loaded too early. // Load our textdomain on init (early) so all init callbacks can translate safely. add_action('init', [$this, 'load_textdomain'], 1); add_action('plugins_loaded', function (): void { try { $this->boot(); } catch (\Throwable $e) { add_action('admin_notices', function () use ($e): void { if (!current_user_can('manage_options')) { return; } echo '

' . esc_html__('2Download Connector for 2DL Hosted Checkout failed during boot. Check your PHP error log for details.', '2download-connector') . '

'; if (defined('WP_DEBUG') && WP_DEBUG) { echo '
'
                            . esc_html($e->getMessage())
                            . "\n\n" . esc_html($e->getFile() . ':' . $e->getLine())
                            . '
'; } }); } }); } public function load_textdomain(): void { // Textdomain loaded automatically by WordPress from languages/ folder } private function register_activation(): void { register_activation_hook(ToDownload_PLUGIN_FILE, function () { Schema::install(); // Ensure Seller role + caps exist immediately on activation. // This prevents sellers from seeing an empty wp-admin menu until init runs. Sellers::ensure_roles_and_caps(); // Ensure the CPT is registered before flushing rewrite rules. // Otherwise, single product pages (e.g. /2download/my-product/) can 404 until permalinks are saved. DownloadPostType::register_post_type(); flush_rewrite_rules(); update_option(self::REWRITE_VERSION_OPTION, ToDownload_VERSION); }); register_deactivation_hook(ToDownload_PLUGIN_FILE, function () { flush_rewrite_rules(); }); } private function maybe_flush_rewrites_once(): void { // Some environments (notably Playground / zip installs) can miss activation hooks. // Do a one-time admin-side flush per plugin version. if (!is_admin() || !current_user_can('manage_options')) { return; } $v = (string) get_option(self::REWRITE_VERSION_OPTION, ''); if ($v === (string) ToDownload_VERSION) { return; } flush_rewrite_rules(); update_option(self::REWRITE_VERSION_OPTION, ToDownload_VERSION); } public function boot(): void { DownloadPostType::register(); $this->maybe_flush_rewrites_once(); Sellers::register(); Blocks::register(); Discounts::register(); Licenses::register(); Reviews::register(); Accounts::register(); Upsells::register(); Schema::maybe_upgrade(); Cart::instance(); GatewayRegistry::instance(); Assets::register(); Checkout::instance(); Shortcodes::register(); DownloadDelivery::instance(); EmailDelivery::instance(); $this->register_frontend_nav_filters(); $this->register_storefront_branding_filters(); $this->register_storefront_page_edit_capability_override(); $this->maybe_migrate_storefront_page_to_block(); $this->maybe_lock_wp_admin_to_ops_only(); if (is_admin()) { Admin::instance(); add_action('admin_notices', [$this, 'maybe_show_setup_notices']); add_action('admin_post_todownload_setup_pages', [$this, 'handle_setup_pages_action']); } } private function ops_only_wp_admin_enabled(): bool { if (defined('TODOWNLOAD_OPS_ONLY_WP_ADMIN')) { return (bool) constant('TODOWNLOAD_OPS_ONLY_WP_ADMIN'); } $env = getenv('TODOWNLOAD_OPS_ONLY_WP_ADMIN'); if (is_string($env)) { $env = strtolower(trim($env)); if (in_array($env, ['1', 'true', 'yes', 'on'], true)) { return true; } if (in_array($env, ['0', 'false', 'no', 'off'], true)) { return false; } } return false; } private function maybe_lock_wp_admin_to_ops_only(): void { if (!$this->ops_only_wp_admin_enabled()) { return; } // Block non-admin logins on this WordPress instance. add_filter('wp_authenticate_user', function ($user) { if ($user instanceof \WP_User) { if (!user_can($user, 'manage_options')) { return new \WP_Error( 'todownload_ops_only', __('Sorry, you are not allowed to access this site.', '2download-connector') ); } } return $user; }, 10, 1); // Extra safety: block any wp-admin access by non-admins (even if they got a session some other way). add_action('admin_init', function (): void { if (!is_user_logged_in()) { return; } if (defined('DOING_AJAX') && DOING_AJAX) { return; } if (!current_user_can('manage_options')) { wp_die(esc_html__('Sorry, you are not allowed to access this page.', '2download-connector')); } }, 0); } private function maybe_migrate_storefront_page_to_block(): void { // One-time migration: replace the raw shortcode with our dynamic Gutenberg block // so the editor can display a live preview. $opt_key = 'ToDownload_storefront_page_block_migrated_v1'; if ((string) get_option($opt_key, '') === '1') { return; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); if ($storefront_page_id <= 0) { $page = get_page_by_path('2download-storefront'); $storefront_page_id = ($page instanceof \WP_Post) ? (int) $page->ID : 0; } if ($storefront_page_id <= 0) { return; } if (!current_user_can('edit_post', $storefront_page_id)) { return; } $post = get_post($storefront_page_id); if (!$post instanceof \WP_Post) { return; } $raw = trim((string) $post->post_content); if ($raw !== '[ToDownload_storefront]') { // Only migrate if it's exactly the legacy shortcode to avoid overwriting customizations. update_option($opt_key, '1'); return; } $block = ''; $res = wp_update_post([ 'ID' => $storefront_page_id, 'post_content' => $block, ], true); if (!is_wp_error($res)) { update_option($opt_key, '1'); } } private function register_storefront_page_edit_capability_override(): void { // Sellers typically don't have edit_pages. Allow editing ONLY the shared Storefront page. add_filter('map_meta_cap', function (array $caps, string $cap, int $user_id, array $args): array { if ($cap !== 'edit_post' && $cap !== 'read_post') { return $caps; } if (empty($args) || empty($args[0])) { return $caps; } $post_id = (int) $args[0]; if ($post_id <= 0) { return $caps; } if (!user_can($user_id, 'edit_todownload_downloads')) { return $caps; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); if ($storefront_page_id <= 0) { $page = get_page_by_path('2download-storefront'); $storefront_page_id = ($page instanceof \WP_Post) ? (int) $page->ID : 0; } if ($storefront_page_id <= 0 || $post_id !== $storefront_page_id) { return $caps; } // Require seller capability instead of edit_pages/edit_post. return ['edit_todownload_downloads']; }, 20, 4); } private function register_frontend_nav_filters(): void { if (is_admin()) { return; } $filter_items = function (array $items): array { if (empty($items)) { return $items; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $cart_page_id = (int) ($settings['cart_page_id'] ?? 0); $account_page_id = (int) ($settings['account_page_id'] ?? 0); $products_page_id = (int) ($settings['products_page_id'] ?? 0); $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); $checkout_page_id = (int) ($settings['checkout_page_id'] ?? 0); $receipt_page_id = 0; $receipt = get_page_by_path('2download-receipt'); if ($receipt instanceof \WP_Post) { $receipt_page_id = (int) $receipt->ID; } $todownload_page_ids = array_values(array_filter([ $cart_page_id, $account_page_id, $products_page_id, $storefront_page_id, $checkout_page_id, $receipt_page_id, ])); $allowed_page_ids = array_values(array_filter([ $storefront_page_id, $account_page_id, $products_page_id, $cart_page_id, ])); $desired_label_by_page_id = array_values(array_filter([ $storefront_page_id > 0 ? [$storefront_page_id, '2DL StoreFront'] : null, $account_page_id > 0 ? [$account_page_id, '2DL Account'] : null, $products_page_id > 0 ? [$products_page_id, '2DL All Products'] : null, $cart_page_id > 0 ? [$cart_page_id, '2DL Cart'] : null, ])); $desired_label_by_page_id = array_reduce($desired_label_by_page_id, function (array $acc, array $pair): array { $acc[(int) $pair[0]] = (string) $pair[1]; return $acc; }, []); // Only apply the whitelist on 2Download pages (or if the menu clearly contains 2Download items). $is_2d_context = false; if (!empty($todownload_page_ids) && is_page($todownload_page_ids)) { $is_2d_context = true; } else if (is_singular(\ToDownload\PostTypes\DownloadPostType::POST_TYPE)) { $is_2d_context = true; } $has_2d_menu_items = false; foreach ($items as $it) { if (!is_object($it)) { continue; } $object = isset($it->object) ? (string) $it->object : ''; $object_id = isset($it->object_id) ? (int) $it->object_id : 0; $title = isset($it->title) ? (string) $it->title : ''; if ($object === 'page' && $object_id > 0 && in_array($object_id, $todownload_page_ids, true)) { $has_2d_menu_items = true; break; } if ($title !== '' && stripos($title, '2download-connector') !== false) { $has_2d_menu_items = true; break; } } if (!$is_2d_context && !$has_2d_menu_items) { return $items; } $allowed_urls = []; foreach ($allowed_page_ids as $pid) { $u = $pid > 0 ? (string) get_permalink($pid) : ''; if ($u !== '') { $allowed_urls[] = untrailingslashit($u); } } $keep_item_ids = []; foreach ($items as $it) { if (!is_object($it)) { continue; } $object = isset($it->object) ? (string) $it->object : ''; $object_id = isset($it->object_id) ? (int) $it->object_id : 0; $url = isset($it->url) ? (string) $it->url : ''; $id = isset($it->ID) ? (int) $it->ID : 0; $is_allowed = false; if ($object === 'page' && $object_id > 0 && in_array($object_id, $allowed_page_ids, true)) { $is_allowed = true; } else if ($url !== '') { $u = untrailingslashit($url); if (in_array($u, $allowed_urls, true)) { $is_allowed = true; } } if ($is_allowed && $id > 0) { $keep_item_ids[] = $id; } } $keep_item_ids = array_values(array_unique(array_filter($keep_item_ids))); if (empty($keep_item_ids)) { return $items; } // Keep children of allowed items. $changed = true; while ($changed) { $changed = false; foreach ($items as $it) { if (!is_object($it)) { continue; } $id = isset($it->ID) ? (int) $it->ID : 0; $parent = isset($it->menu_item_parent) ? (int) $it->menu_item_parent : 0; if ($id > 0 && $parent > 0 && in_array($parent, $keep_item_ids, true) && !in_array($id, $keep_item_ids, true)) { $keep_item_ids[] = $id; $changed = true; } } } $keep_item_ids = array_values(array_unique($keep_item_ids)); $out = []; foreach ($items as $it) { if (!is_object($it)) { $out[] = $it; continue; } $id = isset($it->ID) ? (int) $it->ID : 0; if ($id > 0 && in_array($id, $keep_item_ids, true)) { // Rename the 2Download CTA labels to "2DL ..." when the menu item points at our pages. $object = isset($it->object) ? (string) $it->object : ''; $object_id = isset($it->object_id) ? (int) $it->object_id : 0; if ($object === 'page' && $object_id > 0 && isset($desired_label_by_page_id[$object_id])) { $it->title = $desired_label_by_page_id[$object_id]; if (property_exists($it, 'post_title')) { $it->post_title = $desired_label_by_page_id[$object_id]; } } $out[] = $it; } } // Enforce item order: StoreFront | Account | All Products | Cart. // Keep children directly after their parent. $rank_by_page_id = []; $rank = 0; foreach ([$storefront_page_id, $account_page_id, $products_page_id, $cart_page_id] as $pid) { $pid = (int) $pid; if ($pid > 0) { $rank_by_page_id[$pid] = $rank; $rank++; } } $by_parent = []; foreach ($out as $it) { if (!is_object($it)) { continue; } $parent = isset($it->menu_item_parent) ? (int) $it->menu_item_parent : 0; $by_parent[$parent][] = $it; } $roots = $by_parent[0] ?? []; if (!empty($roots) && !empty($rank_by_page_id)) { usort($roots, function ($a, $b) use ($rank_by_page_id): int { $a_pid = (is_object($a) && isset($a->object) && (string) $a->object === 'page' && isset($a->object_id)) ? (int) $a->object_id : 0; $b_pid = (is_object($b) && isset($b->object) && (string) $b->object === 'page' && isset($b->object_id)) ? (int) $b->object_id : 0; $a_rank = isset($rank_by_page_id[$a_pid]) ? (int) $rank_by_page_id[$a_pid] : 999; $b_rank = isset($rank_by_page_id[$b_pid]) ? (int) $rank_by_page_id[$b_pid] : 999; if ($a_rank === $b_rank) { return 0; } return $a_rank < $b_rank ? -1 : 1; }); $ordered = []; $append_tree = function ($node) use (&$append_tree, &$ordered, $by_parent): void { if (!is_object($node)) { return; } $ordered[] = $node; $id = isset($node->ID) ? (int) $node->ID : 0; if ($id > 0 && !empty($by_parent[$id])) { foreach ($by_parent[$id] as $child) { $append_tree($child); } } }; foreach ($roots as $r) { $append_tree($r); } if (!empty($ordered)) { return $ordered; } } return $out; }; add_filter('wp_nav_menu_objects', function ($items, $args) use ($filter_items) { if (!is_array($items)) { return $items; } return $filter_items($items); }, 20, 2); add_filter('wp_get_nav_menu_items', function ($items, $menu, $args) use ($filter_items) { if (!is_array($items)) { return $items; } return $filter_items($items); }, 20, 3); // Block themes (Navigation block) don't use the classic menu item filters reliably. // Filter the rendered navigation HTML to keep only buyer-facing 2Download CTAs. add_filter('render_block', function (string $block_content, array $block): string { if (($block['blockName'] ?? '') !== 'core/navigation') { return $block_content; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $cart_page_id = (int) ($settings['cart_page_id'] ?? 0); $account_page_id = (int) ($settings['account_page_id'] ?? 0); $products_page_id = (int) ($settings['products_page_id'] ?? 0); $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); $allowed_urls = []; foreach ([$storefront_page_id, $account_page_id, $products_page_id, $cart_page_id] as $pid) { if ($pid > 0) { $u = (string) get_permalink($pid); if ($u !== '') { $allowed_urls[] = untrailingslashit($u); } } } $allowed_urls = array_values(array_unique(array_filter($allowed_urls))); if (empty($allowed_urls)) { return $block_content; } // Only apply if this navigation appears to be the 2Download/buyer nav. // (Avoid touching unrelated navigations.) if ( stripos($block_content, '2download-connector') === false && stripos($block_content, '2Download') === false && stripos($block_content, 'todownload') === false ) { return $block_content; } $normalize = function (string $href): string { $href = trim($href); if ($href === '') { return ''; } // Resolve site-relative URLs. if (strpos($href, '//') === false && strlen($href) > 0 && $href[0] === '/') { $href = home_url($href); } $parts = wp_parse_url($href); if (!is_array($parts)) { return untrailingslashit($href); } $scheme = isset($parts['scheme']) ? (string) $parts['scheme'] : ''; $host = isset($parts['host']) ? (string) $parts['host'] : ''; $path = isset($parts['path']) ? (string) $parts['path'] : ''; if ($scheme === '' || $host === '') { return untrailingslashit($href); } return untrailingslashit($scheme . '://' . $host . $path); }; $allowed_norm = array_map($normalize, $allowed_urls); $allowed_norm = array_values(array_unique(array_filter($allowed_norm))); // Desired labels + order. $desired = []; if ($storefront_page_id > 0) { $desired[] = ['url' => $normalize((string) get_permalink($storefront_page_id)), 'label' => '2DL StoreFront']; } if ($account_page_id > 0) { $desired[] = ['url' => $normalize((string) get_permalink($account_page_id)), 'label' => '2DL Account']; } if ($products_page_id > 0) { $desired[] = ['url' => $normalize((string) get_permalink($products_page_id)), 'label' => '2DL All Products']; } if ($cart_page_id > 0) { $desired[] = ['url' => $normalize((string) get_permalink($cart_page_id)), 'label' => '2DL Cart']; } $desired = array_values(array_filter($desired, function (array $it): bool { return (string) ($it['url'] ?? '') !== ''; })); $desired_url_to_label = []; foreach ($desired as $d) { $desired_url_to_label[(string) $d['url']] = (string) $d['label']; } $desired_order = array_values(array_map(function (array $d): string { return (string) $d['url']; }, $desired)); $html = $block_content; if (!class_exists('DOMDocument')) { return $block_content; } $prev = libxml_use_internal_errors(true); $dom = new \DOMDocument(); // PHP 8.2+: mb_convert_encoding() handling HTML entities is deprecated. // DOMDocument::loadHTML() is not UTF-8 aware by default, so convert non-ASCII // characters to numeric entities to preserve text while keeping markup intact. $encoded = function_exists('mb_encode_numericentity') ? mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8') : $html; $ok = $dom->loadHTML('' . $encoded . ''); libxml_clear_errors(); libxml_use_internal_errors($prev); if (!$ok) { return $block_content; } $xpath = new \DOMXPath($dom); $links = $xpath->query('//a[@href]'); if (!$links) { return $block_content; } $to_remove = []; foreach ($links as $a) { if (!$a instanceof \DOMElement) { continue; } $href = (string) $a->getAttribute('href'); $norm = $normalize($href); if ($norm === '') { continue; } if (in_array($norm, $allowed_norm, true)) { continue; } // Remove the whole navigation item container. $node = $a; while ($node && $node instanceof \DOMElement) { $cls = (string) $node->getAttribute('class'); if (strpos(' ' . $cls . ' ', ' wp-block-navigation-item ') !== false || $node->tagName === 'li') { $to_remove[] = $node; break; } $node = $node->parentNode; } } foreach ($to_remove as $n) { if ($n instanceof \DOMNode && $n->parentNode) { $n->parentNode->removeChild($n); } } // Rename allowed items to "2DL ...". // Special-case Cart to include a count badge that is updated client-side. if (!empty($desired_url_to_label)) { $cart_norm = ''; if ($cart_page_id > 0) { $cart_norm = $normalize((string) get_permalink($cart_page_id)); } foreach ($links as $a) { if (!$a instanceof \DOMElement) { continue; } $href = (string) $a->getAttribute('href'); $norm = $normalize($href); if ($norm === '' || !isset($desired_url_to_label[$norm])) { continue; } // Clear existing children. while ($a->firstChild) { $a->removeChild($a->firstChild); } $label = (string) $desired_url_to_label[$norm]; $a->appendChild($dom->createTextNode($label)); if ($cart_norm !== '' && $norm === $cart_norm) { $a->appendChild($dom->createTextNode(' ')); $badge = $dom->createElement('span', '0'); $badge->setAttribute('class', 'ToDownload-cart-count-badge'); $badge->setAttribute('data-todownload-cart-badge', '1'); $badge->setAttribute('aria-label', 'Cart items: 0'); $a->appendChild($badge); } } } // Reorder top-level navigation items to: StoreFront | Account | All Products | Cart. if (!empty($desired_order)) { $ul = $xpath->query("//nav[contains(concat(' ', normalize-space(@class), ' '), ' wp-block-navigation ')]//ul[1]")->item(0); if ($ul instanceof \DOMElement) { $li_nodes = []; foreach ($ul->childNodes as $child) { if ($child instanceof \DOMElement && strtolower($child->tagName) === 'li') { $li_nodes[] = $child; } } $bucket = []; $rest = []; foreach ($li_nodes as $li) { $a = $xpath->query('.//a[@href][1]', $li)->item(0); $href = ($a instanceof \DOMElement) ? (string) $a->getAttribute('href') : ''; $norm = $href !== '' ? $normalize($href) : ''; if ($norm !== '' && in_array($norm, $desired_order, true)) { $bucket[$norm] = $li; } else { $rest[] = $li; } } // Remove all current LI nodes. foreach ($li_nodes as $li) { if ($li->parentNode) { $li->parentNode->removeChild($li); } } // Append in desired order, then any remaining. foreach ($desired_order as $u) { if (isset($bucket[$u]) && $bucket[$u] instanceof \DOMElement) { $ul->appendChild($bucket[$u]); } } foreach ($rest as $li) { if ($li instanceof \DOMElement) { $ul->appendChild($li); } } } } $body = $dom->getElementsByTagName('body')->item(0); if (!$body) { return $block_content; } $out = ''; foreach ($body->childNodes as $child) { $out .= $dom->saveHTML($child); } return $out !== '' ? $out : $block_content; }, 20, 2); } private function register_storefront_branding_filters(): void { if (is_admin()) { return; } $resolve_storefront_seller = function (): int { static $cached = null; if ($cached !== null) { return (int) $cached; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); if ($storefront_page_id > 0) { if (!is_page($storefront_page_id)) { $cached = 0; return 0; } } else { // Fallback to common slug. if (!is_page('2download-storefront')) { $cached = 0; return 0; } } $qs = ''; if (isset($_GET['ToDownload_seller'])) { // Only honor the seller param when accompanied by a valid nonce. // This is a read-only routing parameter, but we still validate to prevent misuse and satisfy plugin review requirements. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the nonce itself for verification. $nonce = isset($_GET['ToDownload_seller_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_seller_nonce'])) : ''; if ($nonce !== '' && wp_verify_nonce($nonce, 'todownload_storefront_seller')) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce above. $qs = trim(sanitize_text_field((string) wp_unslash($_GET['ToDownload_seller']))); } } if ($qs === '') { $cached = 0; return 0; } $u = null; if (ctype_digit($qs)) { $u = get_user_by('id', (int) $qs); } else { $u = get_user_by('login', $qs); if (!$u) { $u = get_user_by('slug', $qs); } } if (!($u instanceof \WP_User) || empty($u->ID)) { $cached = 0; return 0; } if (!user_can($u, 'edit_todownload_downloads')) { $cached = 0; return 0; } $cached = (int) $u->ID; return (int) $cached; }; add_filter('option_blogname', function ($value) use ($resolve_storefront_seller) { $seller_user_id = $resolve_storefront_seller(); if ($seller_user_id <= 0) { return $value; } $business_name = trim((string) get_user_meta($seller_user_id, \ToDownload\Sellers\Sellers::USER_META_STOREFRONT_BUSINESS_NAME, true)); if ($business_name !== '') { return $business_name; } $u = get_user_by('id', $seller_user_id); $dn = ($u instanceof \WP_User) ? trim((string) $u->display_name) : ''; return $dn !== '' ? $dn : $value; }, 20); add_filter('get_custom_logo', function ($html) use ($resolve_storefront_seller) { $seller_user_id = $resolve_storefront_seller(); if ($seller_user_id <= 0) { return $html; } $logo_url = trim((string) get_user_meta($seller_user_id, \ToDownload\Sellers\Sellers::USER_META_STOREFRONT_LOGO_URL, true)); if ($logo_url === '') { return $html; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $storefront_page_id = (int) ($settings['storefront_page_id'] ?? 0); $storefront_url = ''; if ($storefront_page_id > 0) { $storefront_url = (string) get_permalink($storefront_page_id); } if ($storefront_url === '') { $storefront_url = home_url('/'); } $storefront_url = add_query_arg([ 'ToDownload_seller' => (string) $seller_user_id, 'ToDownload_seller_nonce' => wp_create_nonce('todownload_storefront_seller'), ], $storefront_url); $business_name = trim((string) get_user_meta($seller_user_id, \ToDownload\Sellers\Sellers::USER_META_STOREFRONT_BUSINESS_NAME, true)); if ($business_name === '') { $u = get_user_by('id', $seller_user_id); $business_name = ($u instanceof \WP_User) ? trim((string) $u->display_name) : ''; } $alt = $business_name !== '' ? $business_name : __('Storefront', '2download-connector'); return '' . '' . ''; }, 20); } public function maybe_show_setup_notices(): void { if (!current_user_can('manage_options')) { return; } $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $missing = []; $checkout_page_id = (int) ($settings['checkout_page_id'] ?? 0); $checkout_url = trim((string) ($settings['checkout_url'] ?? '')); if ($checkout_page_id <= 0 && $checkout_url === '') { $missing[] = __('Checkout page', '2download-connector'); } $account_page_id = (int) ($settings['account_page_id'] ?? 0); if ($account_page_id <= 0) { $missing[] = __('Account page', '2download-connector'); } $cart_page_id = (int) ($settings['cart_page_id'] ?? 0); if ($cart_page_id <= 0) { $missing[] = __('Cart page', '2download-connector'); } $success_url = trim((string) ($settings['success_url'] ?? '')); if ($success_url === '') { $missing[] = __('Receipt/Success URL', '2download-connector'); } if (empty($missing)) { $setup_ok = false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified nonce below. if (!empty($_GET['todownload_setup']) && sanitize_key((string) wp_unslash($_GET['todownload_setup'])) === '1') { // Only show the notice when accompanied by a valid nonce. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the nonce itself for verification. $nonce = isset($_GET['todownload_setup_nonce']) ? sanitize_text_field((string) wp_unslash($_GET['todownload_setup_nonce'])) : ''; $setup_ok = ($nonce !== '' && wp_verify_nonce($nonce, 'todownload_setup_notice')); } if ($setup_ok) { echo '

' . esc_html__('2Download setup complete.', '2download-connector') . '

'; } return; } $url = admin_url('admin.php?page=ToDownload_settings'); $action = wp_nonce_url( admin_url('admin-post.php?action=todownload_setup_pages'), 'todownload_setup_pages' ); /* translators: %s = comma-separated list of missing settings */ echo '

' . esc_html(sprintf( /* translators: %s = comma-separated list of missing settings */ __('2Download setup: configure %s in Settings to ensure Cart/Checkout/Account links work.', '2download-connector'), implode(', ', $missing) )) . ' ' . esc_html__('Open Settings', '2download-connector') . '' . ' ' . esc_html__('Auto-create pages', '2download-connector') . '' . '

'; } public function handle_setup_pages_action(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Not allowed.', '2download-connector')); } check_admin_referer('todownload_setup_pages'); $settings = get_option('ToDownload_settings', []); if (!is_array($settings)) { $settings = []; } $cart_page_id = (int) ($settings['cart_page_id'] ?? 0); $checkout_page_id = (int) ($settings['checkout_page_id'] ?? 0); $account_page_id = (int) ($settings['account_page_id'] ?? 0); $success_url = trim((string) ($settings['success_url'] ?? '')); $cancel_url = trim((string) ($settings['cancel_url'] ?? '')); $checkout_url = trim((string) ($settings['checkout_url'] ?? '')); if ($cart_page_id <= 0) { $cart_page_id = $this->ensure_page('2Download Cart', '2download-cart', '[ToDownload_cart]'); } if ($checkout_page_id <= 0) { $checkout_page_id = $this->ensure_page('2Download Checkout', '2download-checkout', '[ToDownload_checkout]'); } if ($account_page_id <= 0) { $account_page_id = $this->ensure_page('2Download Account', '2download-account', '[ToDownload_account]'); } $receipt_page_id = $this->ensure_page('2Download Receipt', '2download-receipt', '[ToDownload_receipt]'); $receipt_url = $receipt_page_id > 0 ? (string) get_permalink($receipt_page_id) : ''; // Dedicated success page (recommended Success URL target). Keep the receipt page for compatibility. $success_page_id = $this->ensure_page('2Download Success', '2download-success', '[ToDownload_receipt]'); $success_page_url = $success_page_id > 0 ? (string) get_permalink($success_page_id) : ''; // Seller growth tools (optional public pages). $products_page_id = $this->ensure_page('2Download All Products', '2download-products', '[ToDownload_all_products]'); $storefront_page_id = $this->ensure_page('2Download Storefront', '2download-storefront', '[ToDownload_storefront]'); $settings['cart_page_id'] = (int) $cart_page_id; $settings['checkout_page_id'] = (int) $checkout_page_id; $settings['account_page_id'] = (int) $account_page_id; if ($products_page_id > 0) { $settings['products_page_id'] = (int) $products_page_id; } if ($storefront_page_id > 0) { $settings['storefront_page_id'] = (int) $storefront_page_id; } if ($checkout_url !== '') { // Prefer page id based routing; clear legacy manual URL if set. $settings['checkout_url'] = ''; } if ($success_url === '') { if ($success_page_url !== '') { $settings['success_url'] = $success_page_url; } elseif ($receipt_url !== '') { $settings['success_url'] = $receipt_url; } } if ($cancel_url === '') { $settings['cancel_url'] = $cart_page_id > 0 ? (string) get_permalink($cart_page_id) : home_url('/'); } update_option('ToDownload_settings', $settings); $redirect = add_query_arg([ 'page' => 'ToDownload_settings', 'todownload_setup' => '1', 'todownload_setup_nonce' => wp_create_nonce('todownload_setup_notice'), ], admin_url('admin.php')); wp_safe_redirect((string) $redirect); exit; } private function ensure_page(string $title, string $slug, string $content): int { $existing = get_page_by_path($slug); if ($existing instanceof \WP_Post) { return (int) $existing->ID; } $page_id = wp_insert_post([ 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => $title, 'post_name' => $slug, 'post_content' => $content, ]); return is_wp_error($page_id) ? 0 : (int) $page_id; } }