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') . '
'
. 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 ''
. '' . esc_html__('2Download setup complete.', '2download-connector') . '
' . 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') . '' . '