| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 36.96% covered (danger) | 36.96% | 17 / 46 | 33.33% covered (danger) | 33.33% | 2 / 6 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
SettingsPage | 36.96% covered (danger) | 36.96% | 17 / 46 | 33.33% covered (danger) | 33.33% | 2 / 6 | 109.45 | 0.00% covered (danger) | 0.00% | 0 / 1
register | 100.00% covered (success) | 100.00% | 3 / 3 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
addMenu | 100.00% covered (success) | 100.00% | 8 / 8 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
render | 85.71% covered (warning) | 85.71% | 6 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.01 |
handleSave | 0.00% covered (danger) | 0.00% | 0 / 14 | 0.00% covered (danger) | 0.00% | 0 / 1 | 72 |
handleRetryActivation | 0.00% covered (danger) | 0.00% | 0 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 42 |
hideSubmenuFromSidebar | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |

1 | <?php
2 | /**
3 | * SettingsPage — F201 (2026-07-30): redirect shim to Operator Dashboard Hub.
4 | *
5 | * Originally this page rendered the operator's SwapAds settings form
6 | * (server URL, license, audience comma-separated fields, auto-inject
7 | * toggle, default placement slug). It has been replaced by:
8 | *
9 | * - License activation: stays here (handleRetryActivation still dispatches
10 | * `swapads_client_retry_activation` for the legacy "Retry activation"
11 | * button).
12 | * - Auto-inject toggle: now lives in OperatorDashboardHubPage::renderSetupTab().
13 | * - Audience fields (sub_niches / geo_buckets / traffic_ranges): now
14 | * checkbox groups in OperatorDashboardHubPage::renderSetupTab(), fetched
15 | * fresh from /v1/audience/definitions.
16 | * - Server URL: removed (auto-discovered via HMAC key exchange).
17 | * - Default placement slug: removed (always 'auto'; shortcode position
18 | * determines actual placement).
19 | *
20 | * render() now redirects to the hub Setup tab for back-compat.
21 | *
22 | * @since 1.0.0
23 | */
24 | declare(strict_types=1);
25 |
26 | namespace SwapAds\Client\Admin;
27 |
28 | use SwapAds\Client\License\FreemiusAutoActivator;
29 | use SwapAds\Client\License\LicenseManager;
30 |
31 | final class SettingsPage
32 | {
33 | public const MENU_SLUG = 'swapads-client';
34 | public const NONCE_ACTION = 'swapads_client_settings';
35 |
36 | /**
37 | * Register WP hooks.
38 | */
39 | public static function register(): void
40 | {
41 | add_action('admin_menu', [self::class, 'addMenu']);
42 | // F2XX-fix 2026-08-01: hide the sidebar link via CSS instead of
43 | // remove_submenu_page() (which broke the cap check in WP 7.0).
44 | add_action('admin_head', [self::class, 'hideSubmenuFromSidebar']);
45 | add_action('admin_post_swapads_client_save', [self::class, 'handleSave']);
46 | }
47 |
48 | /**
49 | * Register the admin menu (top-level). Kept so that any operators who
50 | * bookmark /wp-admin/admin.php?page=swapads-client still land on the
51 | * new hub Setup tab via the render() redirect below.
52 | */
53 | public static function addMenu(): void
54 | {
55 | // F2XX (2026-08-01): re-register the legacy swapads-client slug
56 | // as a HIDDEN submenu under the hub. The page itself just
57 | // redirects to the hub's Setup tab (see render()), but it must
58 | // be routable for old bookmarks + buttons that still link here.
59 | //
60 | // Previous behavior (F233): addMenu() was a no-op, which broke
61 | // the 'Edit Site Settings' button with 'Sorry, you are not
62 | // allowed to access this page' (WP refuses to route to an
63 | // unregistered page slug).
64 | add_submenu_page(
65 | OperatorDashboardHubPage::MENU_SLUG,
66 | 'Site Settings',
67 | 'Site Settings',
68 | 'manage_options',
69 | self::MENU_SLUG,
70 | [self::class, 'render']
71 | );
72 | // Immediately remove from sidebar so the visible submenu count
73 | // stays at 4 (operator spec). Keep the URL routable.
74 | // F2XX-fix 2026-08-01: do NOT call remove_submenu_page() — it breaks
75 | // admin-post.php routing in WP 7.0. Sidebar link is hidden via
76 | // admin_head CSS (see hideSubmenuFromSidebar).
77 | }
78 |
79 | /**
80 | * Render the settings page.
81 | *
82 | * F201 (2026-07-30): settings have moved into the Operator Dashboard Hub's
83 | * "Setup" tab (`?page=swapads-client-operator-hub&tab=setup`). This render()
84 | * now redirects there for back-compat. Operators who bookmark the old
85 | * `?page=swapads-client` URL land on the new Setup tab automatically.
86 | */
87 | public static function render(): void
88 | {
89 | if (!current_user_can('manage_options')) {
90 | wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
91 | }
92 | $dest = add_query_arg(
93 | ['page' => OperatorDashboardHubPage::MENU_SLUG, 'tab' => OperatorDashboardHubPage::TAB_SETUP],
94 | admin_url('admin.php')
95 | );
96 | wp_safe_redirect($dest, 302);
97 | // No exit; here so the function returns normally after the
98 | // redirect header is sent. WP itself calls exit but that's only
99 | // safe in a real request — tests want to assert on the URL.
100 | }
101 |
102 | /**
103 | * Handle the Save Settings POST.
104 | *
105 | * F201 (2026-07-30): only auto_inject + license activation remain
106 | * here. Everything else moved to OperatorDashboardHubPage::handleSaveAudience()
107 | * (Setup tab).
108 | */
109 | public static function handleSave(): void
110 | {
111 | if (!current_user_can('manage_options')) {
112 | return;
113 | }
114 | check_admin_referer(self::NONCE_ACTION);
115 | if (!LicenseManager::isLicensed()) {
116 | $key = trim((string) ($_POST['license_key'] ?? ''));
117 | $secret = trim((string) ($_POST['license_secret'] ?? ''));
118 | if ($key !== '' && $secret !== '') {
119 | // Activate via server (lightweight call)
120 | LicenseManager::store($key, $secret, 0, wp_parse_url(home_url(), PHP_URL_HOST) ?: '', 'pending');
121 | }
122 | }
123 | // F201 (2026-07-30): niche/sub-niche/geo/traffic ranges + default_placement
124 | // removed from this handler. Those fields now live in
125 | // OperatorDashboardHubPage::handleSaveAudience() (Setup tab).
126 | update_option('swapads_client_auto_inject', isset($_POST['auto_inject']) ? 1 : 0);
127 | wp_safe_redirect(add_query_arg(
128 | ['page' => OperatorDashboardHubPage::MENU_SLUG, 'tab' => OperatorDashboardHubPage::TAB_SETUP, 'saved' => '1'],
129 | admin_url('admin.php')
130 | ));
131 | // F2XX (2026-08-01): wp_safe_redirect must be followed by the
132 | // SWAPADS_TESTING_REDIRECT_EXIT-guarded exit; otherwise the admin-post
133 | // handler keeps running and WordPress renders its default "Settings
134 | // saved." page instead of redirecting. Bug 4 from operator feedback
135 | // 2026-08-01: 'edit site settings still doesn't work'.
136 | if (!defined('SWAPADS_TESTING_REDIRECT_EXIT')) { exit; }
137 | }
138 |
139 | /**
140 | * CF17: admin-post handler for the 'Retry activation' button.
141 | *
142 | * Clears the cached activation state and re-runs immediately.
143 | * Redirects back to settings with retry=ok / retry=fail query arg.
144 | */
145 | public static function handleRetryActivation(): void
146 | {
147 | if (!current_user_can('manage_options')) {
148 | wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
149 | }
150 | check_admin_referer('swapads_client_retry_activation');
151 |
152 | try {
153 | $result = FreemiusAutoActivator::retryNow();
154 | $ok = is_array($result) && !empty($result['success']);
155 | } catch (\Throwable) {
156 | $ok = false;
157 | }
158 |
159 | $redirect = add_query_arg(
160 | ['page' => self::MENU_SLUG, 'retry' => $ok ? 'ok' : 'fail'],
161 | admin_url('admin.php')
162 | );
163 | wp_safe_redirect($redirect);
164 | // F2XX (2026-08-01): guarded exit (see Bug 4 fix in handleSave above).
165 | if (!defined('SWAPADS_TESTING_REDIRECT_EXIT')) { exit; }
166 | }
167 |
168 |
169 | /**
170 | * Hide the sidebar link via CSS instead of remove_submenu_page().
171 | *
172 | * F2XX-fix 2026-08-01: WP 7.0's user_can_access_admin_page() iterates
173 | * $submenu[$parent] looking for the slug; if remove_submenu_page() removed
174 | * the entry, the cap check returned false and the operator saw
175 | * 'Sorry, you are not allowed to access this page' on form postbacks.
176 | *
177 | * Keeping the entry in $submenu (so the cap check passes) and hiding the
178 | * visual link via CSS is the correct WP 7.0 pattern.
179 | */
180 | public static function hideSubmenuFromSidebar(): void
181 | {
182 | echo '<style>#adminmenu a[href*="page=swapads-client-settings"]{display:none!important}</style>';
183 | }
184 | }