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 */
24declare(strict_types=1);
25
26namespace SwapAds\Client\Admin;
27
28use SwapAds\Client\License\FreemiusAutoActivator;
29use SwapAds\Client\License\LicenseManager;
30
31final 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}