• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bleedingdeacons / sentinel / 30470902232

29 Jul 2026 04:28PM UTC coverage: 90.517% (+9.2%) from 81.3%
30470902232

push

github

web-flow
test: raise line coverage to ~90% (tests only) (#16)

Close the ~10-point gap to 90% with tests only (no production changes):

- SentinelLoggerEngineTest: the logger engine — buffered dispatch/flush (with
  redaction), the enabled/threshold guards, createTable/truncateTable/dropTable,
  config resolution and request-type detection — driven against the bootstrap's
  $wpdb stand-in (complements SentinelLoggerTest's pure-logic coverage)
- UnityControlPageExtraTest: the runtime-killed and development-mode render
  branches (isolated processes, since they need the real UNITY_KILL / PRODUCTION
  constants), plus the marker-insert and no-op writer branches
- UnityControlPageFailureTest / SettingsPageFailureTest: the write-failure
  branches, reached deterministically by occupying the atomic writer's temp path
  with a directory so file_put_contents() fails while wp-config.php stays
  writable
- StatusDashboardKilledTest: the UNITY_KILL-engaged dashboard render (kill-switch
  badge, dependents stood down, alert help block), in an isolated process

The one previously-fixed defect these lean on: the writers check is_writable on
the same path isWpConfigWritable() does, so the failure branches are otherwise
unreachable from a writable config — the directory-at-temp-path trick exercises
them without a real permission change.

1699 of 1877 relevant lines covered (90.52%)

4.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.78
/src/Admin/UnityControlPage.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sentinel\Admin;
6

7
// Prevent direct access
8
if (!defined('ABSPATH')) {
×
9
    exit;
×
10
}
11

12
use Sentinel\Plugin;
13

14
use function add_action;
15
use function add_settings_error;
16
use function add_submenu_page;
17
use function admin_url;
18
use function check_admin_referer;
19
use function current_user_can;
20
use function esc_attr;
21
use function esc_html;
22
use function esc_html__;
23
use function esc_html_e;
24
use function esc_url;
25
use function settings_errors;
26
use function wp_die;
27
use function wp_json_encode;
28
use function wp_nonce_field;
29

30
/**
31
 * Class UnityControlPage
32
 *
33
 * Sentinel submenu page that controls the Unity plugin's UNITY_KILL kill
34
 * switch by writing a define() line to wp-config.php.
35
 *
36
 * Two actions are exposed:
37
 *   - Disable Unity:  write `define( 'UNITY_KILL', true );` to wp-config.php
38
 *   - Enable Unity:   remove the UNITY_KILL define from wp-config.php
39
 *
40
 * When UNITY_KILL is true Unity short-circuits during boot — no constants,
41
 * no autoloader, no hooks — and every dependent plugin (TSML for Unity,
42
 * Scrutiny, Amber, Integrity, Reconcile) stands down because the
43
 * `unity/loaded` action never fires.
44
 *
45
 * Changes take effect on the next request, since wp-config.php is included
46
 * before plugins on the request that performs the write.
47
 */
48
class UnityControlPage
49
{
50
    private const PAGE_SLUG  = 'sentinel-unity-control';
51
    private const MENU_TITLE = 'Unity Control';
52
    private const PAGE_TITLE = 'Unity Control';
53
    private const CAPABILITY = 'manage_options';
54

55
    private const KILL_SWITCH_CONSTANT = 'UNITY_KILL';
56

57
    /**
58
     * Environment flag constant. When defined as false in wp-config.php,
59
     * developer-only UI is exposed across plugins (e.g. Amber's Developer
60
     * submenu). When undefined or true, the site is treated as production
61
     * and that UI is hidden. The toggle below removes the define when
62
     * switching to production, so wp-config.php only carries the line when
63
     * the site is in the non-default (development) state.
64
     */
65
    private const PRODUCTION_CONSTANT = 'PRODUCTION';
66

67
    /**
68
     * Marker comment placed above the UNITY_KILL define when it is written
69
     * to wp-config.php. Distinct from Sentinel's logger-config marker so
70
     * that the two groups stay visually separate in wp-config.php.
71
     */
72
    private const WP_CONFIG_MARKER = '/* Unity Kill Switch (managed by Sentinel) */';
73

74
    /**
75
     * Marker comment for the PRODUCTION define. Kept distinct from the
76
     * Unity kill-switch marker so the two groups stay visually separate.
77
     */
78
    private const PRODUCTION_MARKER = '/* Environment Flag (managed by Sentinel) */';
79

80
    private const NONCE_ACTION = 'sentinel_unity_control';
81
    private const NONCE_FIELD  = '_sentinel_unity_nonce';
82

83
    /**
84
     * Plugins that depend on Unity and will stop working when the kill
85
     * switch is engaged. Used in the warning copy on the page.
86
     *
87
     * Concordance is intentionally not in this list: it does not depend
88
     * on Unity.
89
     *
90
     * @var list<string>
91
     */
92
    private const DEPENDENT_PLUGINS = [
93
        'TSML for Unity',
94
        'Scrutiny',
95
        'Amber',
96
        'Integrity',
97
        'Reconcile',
98
        'Reach',
99
        'Stalwart',
100
        'Steward',
101
        'Trusted',
102
        'Trumpet',
103
    ];
104

105
    /** @var string The hook suffix returned by add_submenu_page(). */
106

107
    /**
108
     * Set to true after a successful enable/disable in handleSave(), so
109
     * renderPage() can emit a small script that reloads the page (and
110
     * thereby picks up the new runtime state) after briefly showing the
111
     * success notice.
112
     */
113
    private static bool $justChanged = false;
114

115
    public static function init(): void
3 ✔
116
    {
117
        add_action('admin_menu', [self::class, 'registerPage']);
3 ✔
118
        add_action('admin_init', [self::class, 'handleSave']);
3 ✔
119
    }
120

121
    public static function registerPage(): void
1 ✔
122
    {
123
        add_submenu_page(
1 ✔
124
            Plugin::MENU_SLUG,
1 ✔
125
            self::PAGE_TITLE,
1 ✔
126
            self::MENU_TITLE,
1 ✔
127
            self::CAPABILITY,
1 ✔
128
            self::PAGE_SLUG,
1 ✔
129
            [self::class, 'renderPage']
1 ✔
130
        );
1 ✔
131
    }
132

133
    // ── Save Handler ───────────────────────────────────────────────────────────
134

135
    /**
136
     * Handle form submission from the Unity Control page.
137
     *
138
     * Two actions are accepted via the `sentinel_unity_action` POST field:
139
     *   - 'disable' — write UNITY_KILL=true to wp-config.php (requires the
140
     *                 'sentinel_unity_confirm' checkbox)
141
     *   - 'enable'  — remove the UNITY_KILL define from wp-config.php
142
     */
143
    public static function handleSave(): void
26 ✔
144
    {
145
        if (!isset($_POST[self::NONCE_FIELD])) {
26 ✔
146
            return;
1 ✔
147
        }
148

149
        if (!current_user_can(self::CAPABILITY)) {
25 ✔
150
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'sentinel'));
1 ✔
151
        }
152

153
        check_admin_referer(self::NONCE_ACTION, self::NONCE_FIELD);
24 ✔
154

155
        $action = isset($_POST['sentinel_unity_action'])
24 ✔
156
            ? (string) $_POST['sentinel_unity_action']
24 ✔
157
            : '';
×
158

159
        if (!in_array($action, ['disable', 'enable', 'production_on', 'production_off'], true)) {
24 ✔
160
            return;
1 ✔
161
        }
162

163
        if (!SettingsPage::isWpConfigWritable()) {
23 ✔
164
            add_settings_error(
1 ✔
165
                'sentinel_unity_control',
1 ✔
166
                'wp_config_not_writable',
1 ✔
167
                __('wp-config.php is not writable. The kill switch could not be changed from this page. See the manual instructions below.', 'sentinel'),
1 ✔
168
                'error'
1 ✔
169
            );
1 ✔
170
            return;
1 ✔
171
        }
172

173
        if ($action === 'disable') {
22 ✔
174
            // Require explicit confirmation, since this disables six
175
            // dependent plugins.
176
            if (empty($_POST['sentinel_unity_confirm'])) {
9 ✔
177
                add_settings_error(
1 ✔
178
                    'sentinel_unity_control',
1 ✔
179
                    'confirm_required',
1 ✔
180
                    __('You must tick the confirmation checkbox before disabling Unity.', 'sentinel'),
1 ✔
181
                    'error'
1 ✔
182
                );
1 ✔
183
                return;
1 ✔
184
            }
185

186
            if (self::writeKillSwitch(true)) {
8 ✔
187
                self::$justChanged = true;
7 ✔
188
                add_settings_error(
7 ✔
189
                    'sentinel_unity_control',
7 ✔
190
                    'kill_switch_enabled',
7 ✔
191
                    __('Unity has been disabled. The change takes effect on the next page load. Dependent plugins (TSML for Unity, Scrutiny, Amber, Integrity, Reconcile, Reach, Stalwart, Steward, Trusted, Trumpet) will stand down.', 'sentinel'),
7 ✔
192
                    'updated'
7 ✔
193
                );
7 ✔
194
            } else {
195
                add_settings_error(
1 ✔
196
                    'sentinel_unity_control',
1 ✔
197
                    'write_failed',
1 ✔
198
                    __('Failed to write the kill switch to wp-config.php. The file may have become unwritable.', 'sentinel'),
1 ✔
199
                    'error'
1 ✔
200
                );
1 ✔
201
            }
202
            return;
8 ✔
203
        }
204

205
        if ($action === 'enable') {
13 ✔
206
            if (self::removeKillSwitch()) {
3 ✔
207
                self::$justChanged = true;
2 ✔
208
                add_settings_error(
2 ✔
209
                    'sentinel_unity_control',
2 ✔
210
                    'kill_switch_removed',
2 ✔
211
                    __('Unity has been re-enabled, and is loading now.', 'sentinel'),
2 ✔
212
                    'updated'
2 ✔
213
                );
2 ✔
214
            } else {
215
                add_settings_error(
1 ✔
216
                    'sentinel_unity_control',
1 ✔
217
                    'remove_failed',
1 ✔
218
                    __('Failed to remove the kill switch from wp-config.php. The file may have become unwritable.', 'sentinel'),
1 ✔
219
                    'error'
1 ✔
220
                );
1 ✔
221
            }
222
            return;
3 ✔
223
        }
224

225
        // ── PRODUCTION toggle ──
226
        // 'production_off' writes define( 'PRODUCTION', false ); to expose
227
        // developer UI. 'production_on' removes the define so the runtime
228
        // default (true) takes over, keeping wp-config.php clean when in
229
        // the default production state.
230

231
        if ($action === 'production_off') {
10 ✔
232
            if (self::writeProduction(false)) {
7 ✔
233
                self::$justChanged = true;
6 ✔
234
                add_settings_error(
6 ✔
235
                    'sentinel_unity_control',
6 ✔
236
                    'production_off',
6 ✔
237
                    __('Production mode disabled. Developer-only UI will be exposed on the next page load.', 'sentinel'),
6 ✔
238
                    'updated'
6 ✔
239
                );
6 ✔
240
            } else {
241
                add_settings_error(
1 ✔
242
                    'sentinel_unity_control',
1 ✔
243
                    'production_write_failed',
1 ✔
244
                    __('Failed to write the PRODUCTION constant to wp-config.php. The file may have become unwritable.', 'sentinel'),
1 ✔
245
                    'error'
1 ✔
246
                );
1 ✔
247
            }
248
            return;
7 ✔
249
        }
250

251
        // action === 'production_on'
252
        if (self::removeProduction()) {
3 ✔
253
            self::$justChanged = true;
2 ✔
254
            add_settings_error(
2 ✔
255
                'sentinel_unity_control',
2 ✔
256
                'production_on',
2 ✔
257
                __('Production mode enabled. Developer-only UI will be hidden on the next page load.', 'sentinel'),
2 ✔
258
                'updated'
2 ✔
259
            );
2 ✔
260
        } else {
261
            add_settings_error(
1 ✔
262
                'sentinel_unity_control',
1 ✔
263
                'production_remove_failed',
1 ✔
264
                __('Failed to remove the PRODUCTION constant from wp-config.php. The file may have become unwritable.', 'sentinel'),
1 ✔
265
                'error'
1 ✔
266
            );
1 ✔
267
        }
268
    }
269

270
    // ── wp-config.php Writers ──────────────────────────────────────────────────
271

272
    /**
273
     * Write `define( 'UNITY_KILL', <bool> );` to wp-config.php under the
274
     * Unity-specific marker comment.
275
     *
276
     * Replaces the existing line if present; inserts a new marker + define
277
     * after the opening <?php tag otherwise. Built on top of the atomic
278
     * write primitive shared with SettingsPage.
279
     */
280
    private static function writeKillSwitch(bool $value): bool
8 ✔
281
    {
282
        $path = SettingsPage::wpConfigPath();
8 ✔
283
        if ($path === null || !is_writable($path)) {
8 ✔
284
            return false;
×
285
        }
286

287
        $contents = file_get_contents($path);
8 ✔
288
        if ($contents === false) {
8 ✔
289
            return false;
×
290
        }
291

292
        $defineLine = "define( '" . self::KILL_SWITCH_CONSTANT . "', " . ($value ? 'true' : 'false') . " );";
8 ✔
293

294
        $pattern = '/^\s*define\s*\(\s*[\'"]'
8 ✔
295
            . preg_quote(self::KILL_SWITCH_CONSTANT, '/')
8 ✔
296
            . '[\'"]\s*,\s*.+?\)\s*;/m';
8 ✔
297

298
        if (preg_match($pattern, $contents)) {
8 ✔
299
            // Replace existing line in-place.
300
            $updated = preg_replace($pattern, $defineLine, $contents, 1);
2 ✔
301
        } elseif (str_contains($contents, self::WP_CONFIG_MARKER)) {
6 ✔
302
            // Marker is present from a previous run — insert under it.
303
            $updated = preg_replace(
1 ✔
304
                '/' . preg_quote(self::WP_CONFIG_MARKER, '/') . '/m',
1 ✔
305
                self::WP_CONFIG_MARKER . "\n" . $defineLine,
1 ✔
306
                $contents,
1 ✔
307
                1
1 ✔
308
            );
1 ✔
309
        } else {
310
            // Fresh insert after the opening <?php tag.
311
            $insertBlock = "\n" . self::WP_CONFIG_MARKER . "\n" . $defineLine . "\n";
5 ✔
312
            $pos = strpos($contents, '<?php');
5 ✔
313
            if ($pos !== false) {
5 ✔
314
                $eol = strpos($contents, "\n", $pos);
4 ✔
315
                if ($eol !== false) {
4 ✔
316
                    $updated = substr_replace($contents, $insertBlock, $eol + 1, 0);
3 ✔
317
                } else {
318
                    $updated = $contents . $insertBlock;
4 ✔
319
                }
320
            } else {
321
                // Defensive fallback — wp-config.php should always start
322
                // with <?php.
323
                $updated = "<?php\n" . $insertBlock . $contents;
1 ✔
324
            }
325
        }
326

327
        if ($updated === null || $updated === $contents) {
8 ✔
328
            return $updated !== null; // No-op success vs. preg failure.
1 ✔
329
        }
330

331
        return self::atomicWrite($path, $updated);
7 ✔
332
    }
333

334
    /**
335
     * Remove `define( 'UNITY_KILL', ... );` and its marker comment from
336
     * wp-config.php. Idempotent — returns true if there was nothing to
337
     * remove.
338
     */
339
    private static function removeKillSwitch(): bool
3 ✔
340
    {
341
        $path = SettingsPage::wpConfigPath();
3 ✔
342
        if ($path === null || !is_writable($path)) {
3 ✔
343
            return false;
×
344
        }
345

346
        $contents = file_get_contents($path);
3 ✔
347
        if ($contents === false) {
3 ✔
348
            return false;
×
349
        }
350

351
        $pattern = '/^\s*define\s*\(\s*[\'"]'
3 ✔
352
            . preg_quote(self::KILL_SWITCH_CONSTANT, '/')
3 ✔
353
            . '[\'"]\s*,\s*.+?\)\s*;\s*\n?/m';
3 ✔
354
        $updated = preg_replace($pattern, '', $contents);
3 ✔
355

356
        if ($updated === null) {
3 ✔
357
            return false;
×
358
        }
359

360
        // Remove the marker comment if it now stands alone.
361
        $updated = str_replace(self::WP_CONFIG_MARKER . "\n", '', $updated);
3 ✔
362
        $updated = str_replace(self::WP_CONFIG_MARKER, '', $updated);
3 ✔
363

364
        // Tidy excessive blank lines left by the removal.
365
        $updated = preg_replace("/\n{3,}/", "\n\n", $updated);
3 ✔
366

367
        if ($updated === $contents) {
3 ✔
368
            return true; // Nothing was set; nothing to do.
1 ✔
369
        }
370

371
        return self::atomicWrite($path, $updated);
2 ✔
372
    }
373

374
    /**
375
     * Atomic write: write to a temp file in the same directory, preserve
376
     * the original permissions, then rename into place.
377
     */
378
    private static function atomicWrite(string $path, string $contents): bool
17 ✔
379
    {
380
        $tmpPath = $path . '.sentinel-unity-tmp';
17 ✔
381
        if (file_put_contents($tmpPath, $contents) === false) {
17 ✔
382
            return false;
4 ✔
383
        }
384

385
        $perms = fileperms($path);
13 ✔
386
        if ($perms !== false) {
13 ✔
387
            chmod($tmpPath, $perms);
13 ✔
388
        }
389

390
        return rename($tmpPath, $path);
13 ✔
391
    }
392

393
    // ── State Inspection ───────────────────────────────────────────────────────
394

395
    /**
396
     * Whether the kill switch is currently active at runtime.
397
     *
398
     * Mirrors the check in unity/unity.php exactly — only the literal
399
     * boolean `true` engages the kill switch.
400
     */
401
    private static function isKilledAtRuntime(): bool
8 ✔
402
    {
403
        return defined(self::KILL_SWITCH_CONSTANT) && constant(self::KILL_SWITCH_CONSTANT) === true;
8 ✔
404
    }
405

406
    /**
407
     * Whether wp-config.php currently contains a UNITY_KILL define on
408
     * disk, irrespective of its value. Used to advise the operator whether
409
     * the "Enable" action will actually do anything.
410
     */
411
    private static function isDefinedInFile(): bool
8 ✔
412
    {
413
        $path = SettingsPage::wpConfigPath();
8 ✔
414
        if ($path === null || !is_readable($path)) {
8 ✔
415
            return false;
1 ✔
416
        }
417

418
        $contents = file_get_contents($path);
7 ✔
419
        if ($contents === false) {
7 ✔
420
            return false;
×
421
        }
422

423
        $pattern = '/^\s*define\s*\(\s*[\'"]'
7 ✔
424
            . preg_quote(self::KILL_SWITCH_CONSTANT, '/')
7 ✔
425
            . '[\'"]\s*,/m';
7 ✔
426

427
        return preg_match($pattern, $contents) === 1;
7 ✔
428
    }
429

430
    // ── PRODUCTION wp-config.php Writers ───────────────────────────────────────
431

432
    /**
433
     * Write `define( 'PRODUCTION', <bool> );` to wp-config.php under the
434
     * PRODUCTION-specific marker comment. Mirrors writeKillSwitch().
435
     */
436
    private static function writeProduction(bool $value): bool
7 ✔
437
    {
438
        $path = SettingsPage::wpConfigPath();
7 ✔
439
        if ($path === null || !is_writable($path)) {
7 ✔
440
            return false;
×
441
        }
442

443
        $contents = file_get_contents($path);
7 ✔
444
        if ($contents === false) {
7 ✔
445
            return false;
×
446
        }
447

448
        $defineLine = "define( '" . self::PRODUCTION_CONSTANT . "', " . ($value ? 'true' : 'false') . " );";
7 ✔
449

450
        $pattern = '/^\s*define\s*\(\s*[\'"]'
7 ✔
451
            . preg_quote(self::PRODUCTION_CONSTANT, '/')
7 ✔
452
            . '[\'"]\s*,\s*.+?\)\s*;/m';
7 ✔
453

454
        if (preg_match($pattern, $contents)) {
7 ✔
455
            $updated = preg_replace($pattern, $defineLine, $contents, 1);
2 ✔
456
        } elseif (str_contains($contents, self::PRODUCTION_MARKER)) {
5 ✔
457
            $updated = preg_replace(
1 ✔
458
                '/' . preg_quote(self::PRODUCTION_MARKER, '/') . '/m',
1 ✔
459
                self::PRODUCTION_MARKER . "\n" . $defineLine,
1 ✔
460
                $contents,
1 ✔
461
                1
1 ✔
462
            );
1 ✔
463
        } else {
464
            $insertBlock = "\n" . self::PRODUCTION_MARKER . "\n" . $defineLine . "\n";
4 ✔
465
            $pos = strpos($contents, '<?php');
4 ✔
466
            if ($pos !== false) {
4 ✔
467
                $eol = strpos($contents, "\n", $pos);
3 ✔
468
                if ($eol !== false) {
3 ✔
469
                    $updated = substr_replace($contents, $insertBlock, $eol + 1, 0);
2 ✔
470
                } else {
471
                    $updated = $contents . $insertBlock;
3 ✔
472
                }
473
            } else {
474
                $updated = "<?php\n" . $insertBlock . $contents;
1 ✔
475
            }
476
        }
477

478
        if ($updated === null || $updated === $contents) {
7 ✔
479
            return $updated !== null;
1 ✔
480
        }
481

482
        return self::atomicWrite($path, $updated);
6 ✔
483
    }
484

485
    /**
486
     * Remove the PRODUCTION define and its marker from wp-config.php.
487
     * Idempotent. Mirrors removeKillSwitch().
488
     */
489
    private static function removeProduction(): bool
3 ✔
490
    {
491
        $path = SettingsPage::wpConfigPath();
3 ✔
492
        if ($path === null || !is_writable($path)) {
3 ✔
493
            return false;
×
494
        }
495

496
        $contents = file_get_contents($path);
3 ✔
497
        if ($contents === false) {
3 ✔
498
            return false;
×
499
        }
500

501
        $pattern = '/^\s*define\s*\(\s*[\'"]'
3 ✔
502
            . preg_quote(self::PRODUCTION_CONSTANT, '/')
3 ✔
503
            . '[\'"]\s*,\s*.+?\)\s*;\s*\n?/m';
3 ✔
504
        $updated = preg_replace($pattern, '', $contents);
3 ✔
505

506
        if ($updated === null) {
3 ✔
507
            return false;
×
508
        }
509

510
        $updated = str_replace(self::PRODUCTION_MARKER . "\n", '', $updated);
3 ✔
511
        $updated = str_replace(self::PRODUCTION_MARKER, '', $updated);
3 ✔
512

513
        $updated = preg_replace("/\n{3,}/", "\n\n", $updated);
3 ✔
514

515
        if ($updated === $contents) {
3 ✔
516
            return true;
1 ✔
517
        }
518

519
        return self::atomicWrite($path, $updated);
2 ✔
520
    }
521

522
    /**
523
     * Whether the site is currently in production mode at runtime.
524
     *
525
     * PRODUCTION defaults to true when not defined, so undefined ≡ production.
526
     */
527
    private static function isProductionAtRuntime(): bool
8 ✔
528
    {
529
        return !defined(self::PRODUCTION_CONSTANT) || constant(self::PRODUCTION_CONSTANT) === true;
8 ✔
530
    }
531

532
    /**
533
     * Whether wp-config.php currently contains a PRODUCTION define on disk,
534
     * irrespective of its value.
535
     */
536
    private static function isProductionDefinedInFile(): bool
8 ✔
537
    {
538
        $path = SettingsPage::wpConfigPath();
8 ✔
539
        if ($path === null || !is_readable($path)) {
8 ✔
540
            return false;
1 ✔
541
        }
542

543
        $contents = file_get_contents($path);
7 ✔
544
        if ($contents === false) {
7 ✔
545
            return false;
×
546
        }
547

548
        $pattern = '/^\s*define\s*\(\s*[\'"]'
7 ✔
549
            . preg_quote(self::PRODUCTION_CONSTANT, '/')
7 ✔
550
            . '[\'"]\s*,/m';
7 ✔
551

552
        return preg_match($pattern, $contents) === 1;
7 ✔
553
    }
554

555
    // ── Page Rendering ─────────────────────────────────────────────────────────
556

557
    public static function renderPage(): void
9 ✔
558
    {
559
        if (!current_user_can(self::CAPABILITY)) {
9 ✔
560
            wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'sentinel'));
1 ✔
561
        }
562

563
        $killed         = self::isKilledAtRuntime();
8 ✔
564
        $inFile         = self::isDefinedInFile();
8 ✔
565
        $isProduction   = self::isProductionAtRuntime();
8 ✔
566
        $prodInFile     = self::isProductionDefinedInFile();
8 ✔
567
        $writable       = SettingsPage::isWpConfigWritable();
8 ✔
568
        $configPath     = SettingsPage::wpConfigPath();
8 ✔
569
        ?>
570
        <div class="wrap">
8 ✔
571
            <h1><?php echo esc_html(self::PAGE_TITLE); ?></h1>
8 ✔
572

573
            <?php settings_errors('sentinel_unity_control'); ?>
8 ✔
574

575
            <?php if (self::$justChanged): ?>
8 ✔
576
                <?php
1 ✔
577
                // After a successful enable/disable we briefly show the
578
                // success notice and then reload. The reload is essential:
579
                // the UNITY_KILL define is read from wp-config.php, which
580
                // is included before plugins, so the page rendered by the
581
                // same request that wrote it still reflects the *old*
582
                // runtime state. A GET reload picks up the new state and
583
                // avoids the browser's POST-resubmit prompt.
584
                $reloadUrl = esc_url(
1 ✔
585
                    admin_url('admin.php?page=' . self::PAGE_SLUG)
1 ✔
586
                );
1 ✔
587
                ?>
588
                <script>
1 ✔
589
                (function () {
1 ✔
590
                    setTimeout(function () {
1 ✔
591
                        window.location.replace(<?php echo wp_json_encode($reloadUrl); ?>);
1 ✔
592
                    }, 1500);
593
                })();
594
                </script>
595
            <?php endif; ?>
596

597
            <p>
598
                <?php
599
                printf(
8 ✔
600
                    esc_html__('Unity supports a %s kill switch defined in %s. When set to %s, Unity short-circuits during boot — no constants, no autoloader, no hooks — and every dependent plugin stands down.', 'sentinel'),
8 ✔
601
                    '<code>UNITY_KILL</code>',
8 ✔
602
                    '<code>wp-config.php</code>',
8 ✔
603
                    '<code>true</code>'
8 ✔
604
                );
8 ✔
605
                ?>
606
            </p>
8 ✔
607

608
            <!-- Current State -->
8 ✔
609
            <h2><?php esc_html_e('Current State', 'sentinel'); ?></h2>
8 ✔
610
            <table class="widefat striped" style="max-width:720px;">
611
                <tbody>
612
                    <tr>
613
                        <th scope="row" style="width:40%;">
614
                            <?php esc_html_e('Runtime status', 'sentinel'); ?>
8 ✔
615
                        </th>
8 ✔
616
                        <td>
8 ✔
617
                            <?php if ($killed): ?>
8 ✔
618
                                <strong style="color:#d63638;">
2 ✔
619
                                    <?php esc_html_e('Disabled (kill switch engaged)', 'sentinel'); ?>
2 ✔
620
                                </strong>
2 ✔
621
                            <?php else: ?>
2 ✔
622
                                <strong style="color:#00a32a;">
6 ✔
623
                                    <?php esc_html_e('Enabled', 'sentinel'); ?>
6 ✔
624
                                </strong>
6 ✔
625
                            <?php endif; ?>
6 ✔
626
                        </td>
8 ✔
627
                    </tr>
8 ✔
628
                    <tr>
8 ✔
629
                        <th scope="row">
8 ✔
630
                            <?php
8 ✔
631
                            printf(
8 ✔
632
                                esc_html__('%s in wp-config.php', 'sentinel'),
8 ✔
633
                                '<code>UNITY_KILL</code>'
8 ✔
634
                            );
8 ✔
635
                            ?>
636
                        </th>
8 ✔
637
                        <td>
8 ✔
638
                            <?php if ($inFile): ?>
8 ✔
639
                                <?php esc_html_e('Defined', 'sentinel'); ?>
3 ✔
640
                            <?php else: ?>
3 ✔
641
                                <?php esc_html_e('Not defined', 'sentinel'); ?>
5 ✔
642
                            <?php endif; ?>
5 ✔
643
                        </td>
8 ✔
644
                    </tr>
8 ✔
645
                    <tr>
8 ✔
646
                        <th scope="row"><?php esc_html_e('wp-config.php location', 'sentinel'); ?></th>
8 ✔
647
                        <td>
648
                            <?php if ($configPath !== null): ?>
8 ✔
649
                                <code><?php echo esc_html($configPath); ?></code>
7 ✔
650
                            <?php else: ?>
651
                                <em><?php esc_html_e('Not found', 'sentinel'); ?></em>
1 ✔
652
                            <?php endif; ?>
653
                        </td>
8 ✔
654
                    </tr>
8 ✔
655
                    <tr>
8 ✔
656
                        <th scope="row"><?php esc_html_e('Writable by web server', 'sentinel'); ?></th>
8 ✔
657
                        <td>
658
                            <?php if ($writable): ?>
8 ✔
659
                                <?php esc_html_e('Yes', 'sentinel'); ?>
7 ✔
660
                            <?php else: ?>
7 ✔
661
                                <strong><?php esc_html_e('No', 'sentinel'); ?></strong>
1 ✔
662
                                — <?php esc_html_e('toggle controls below are unavailable; use the manual instructions.', 'sentinel'); ?>
1 ✔
663
                            <?php endif; ?>
1 ✔
664
                        </td>
8 ✔
665
                    </tr>
8 ✔
666
                </tbody>
8 ✔
667
            </table>
8 ✔
668

669
            <?php if ($killed && !$inFile): ?>
8 ✔
670
                <div class="notice notice-warning inline" style="margin-top:1em;">
1 ✔
671
                    <p>
1 ✔
672
                        <?php
1 ✔
673
                        printf(
1 ✔
674
                            esc_html__('%s is currently true at runtime but no matching define was found in %s. The constant is being set somewhere else (a must-use plugin, a server-level config, or another file). The controls below cannot manage it.', 'sentinel'),
1 ✔
675
                            '<code>UNITY_KILL</code>',
1 ✔
676
                            '<code>wp-config.php</code>'
1 ✔
677
                        );
1 ✔
678
                        ?>
679
                    </p>
1 ✔
680
                </div>
1 ✔
681
            <?php endif; ?>
1 ✔
682

683
            <!-- Disable / Enable controls -->
8 ✔
684
            <h2><?php esc_html_e('Toggle Unity', 'sentinel'); ?></h2>
8 ✔
685

686
            <?php if ($killed): ?>
8 ✔
687
                <p>
2 ✔
688
                    <?php esc_html_e('Unity is currently disabled. Re-enabling will remove the kill switch from wp-config.php and Unity will boot normally on the next page load.', 'sentinel'); ?>
2 ✔
689
                </p>
2 ✔
690
                <form method="post" action="">
2 ✔
691
                    <?php wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD); ?>
2 ✔
692
                    <input type="hidden" name="sentinel_unity_action" value="enable">
2 ✔
693
                    <p>
2 ✔
694
                        <button type="submit" class="button button-primary" <?php echo $writable ? '' : 'disabled'; ?>>
2 ✔
695
                            <?php esc_html_e('Enable Unity', 'sentinel'); ?>
2 ✔
696
                        </button>
2 ✔
697
                    </p>
2 ✔
698
                </form>
2 ✔
699
            <?php else: ?>
2 ✔
700
                <div class="notice notice-error inline" style="margin:1em 0;">
6 ✔
701
                    <p>
6 ✔
702
                        <strong><?php esc_html_e('Disabling Unity will also disable:', 'sentinel'); ?></strong>
6 ✔
703
                        <?php echo esc_html(implode(', ', self::DEPENDENT_PLUGINS)); ?>.
6 ✔
704
                        <?php esc_html_e('These plugins depend on Unity having booted, and will stand down until the kill switch is cleared.', 'sentinel'); ?>
6 ✔
705
                    </p>
6 ✔
706
                </div>
6 ✔
707
                <form method="post" action="">
6 ✔
708
                    <?php wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD); ?>
6 ✔
709
                    <input type="hidden" name="sentinel_unity_action" value="disable">
6 ✔
710
                    <p>
6 ✔
711
                        <label>
6 ✔
712
                            <input type="checkbox" name="sentinel_unity_confirm" value="1">
6 ✔
713
                            <?php esc_html_e('I understand this will disable Unity and every plugin that depends on it.', 'sentinel'); ?>
6 ✔
714
                        </label>
6 ✔
715
                    </p>
6 ✔
716
                    <p>
6 ✔
717
                        <button type="submit" class="button button-primary" <?php echo $writable ? '' : 'disabled'; ?>>
6 ✔
718
                            <?php esc_html_e('Disable Unity', 'sentinel'); ?>
6 ✔
719
                        </button>
6 ✔
720
                    </p>
6 ✔
721
                </form>
6 ✔
722
            <?php endif; ?>
6 ✔
723

724
            <!-- Production Mode toggle -->
8 ✔
725
            <h2><?php esc_html_e('Production Mode', 'sentinel'); ?></h2>
8 ✔
726
            <p>
727
                <?php
728
                printf(
8 ✔
729
                    esc_html__('The %s constant controls whether developer-only UI (such as Amber\'s Developer submenu) is exposed. Defaults to %s when not defined.', 'sentinel'),
8 ✔
730
                    '<code>PRODUCTION</code>',
8 ✔
731
                    '<code>true</code>'
8 ✔
732
                );
8 ✔
733
                ?>
734
            </p>
8 ✔
735

736
            <table class="widefat striped" style="max-width:720px;">
8 ✔
737
                <tbody>
8 ✔
738
                    <tr>
8 ✔
739
                        <th scope="row" style="width:40%;">
8 ✔
740
                            <?php esc_html_e('Current mode', 'sentinel'); ?>
8 ✔
741
                        </th>
8 ✔
742
                        <td>
8 ✔
743
                            <?php if ($isProduction): ?>
8 ✔
744
                                <strong style="color:#00a32a;">
7 ✔
745
                                    <?php esc_html_e('Production (developer UI hidden)', 'sentinel'); ?>
7 ✔
746
                                </strong>
7 ✔
747
                            <?php else: ?>
7 ✔
748
                                <strong style="color:#d63638;">
1 ✔
749
                                    <?php esc_html_e('Development (developer UI exposed)', 'sentinel'); ?>
1 ✔
750
                                </strong>
1 ✔
751
                            <?php endif; ?>
1 ✔
752
                        </td>
8 ✔
753
                    </tr>
8 ✔
754
                    <tr>
8 ✔
755
                        <th scope="row">
8 ✔
756
                            <?php
8 ✔
757
                            printf(
8 ✔
758
                                esc_html__('%s in wp-config.php', 'sentinel'),
8 ✔
759
                                '<code>PRODUCTION</code>'
8 ✔
760
                            );
8 ✔
761
                            ?>
762
                        </th>
8 ✔
763
                        <td>
8 ✔
764
                            <?php if ($prodInFile): ?>
8 ✔
765
                                <?php esc_html_e('Defined', 'sentinel'); ?>
1 ✔
766
                            <?php else: ?>
1 ✔
767
                                <?php esc_html_e('Not defined (using default: true)', 'sentinel'); ?>
7 ✔
768
                            <?php endif; ?>
7 ✔
769
                        </td>
8 ✔
770
                    </tr>
8 ✔
771
                </tbody>
8 ✔
772
            </table>
8 ✔
773

774
            <?php if (!$isProduction && !$prodInFile): ?>
8 ✔
775
                <div class="notice notice-warning inline" style="margin-top:1em;">
1 ✔
776
                    <p>
1 ✔
777
                        <?php
1 ✔
778
                        printf(
1 ✔
779
                            esc_html__('%s is currently false at runtime but no matching define was found in %s. The constant is being set somewhere else (a must-use plugin, a server-level config, or another file). The controls below cannot manage it.', 'sentinel'),
1 ✔
780
                            '<code>PRODUCTION</code>',
1 ✔
781
                            '<code>wp-config.php</code>'
1 ✔
782
                        );
1 ✔
783
                        ?>
784
                    </p>
1 ✔
785
                </div>
1 ✔
786
            <?php endif; ?>
1 ✔
787

788
            <?php if ($isProduction): ?>
8 ✔
789
                <p>
7 ✔
790
                    <?php esc_html_e('Switching to development mode will write define( \'PRODUCTION\', false ); to wp-config.php and expose developer-only UI on the next page load.', 'sentinel'); ?>
7 ✔
791
                </p>
7 ✔
792
                <form method="post" action="">
7 ✔
793
                    <?php wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD); ?>
7 ✔
794
                    <input type="hidden" name="sentinel_unity_action" value="production_off">
7 ✔
795
                    <p>
7 ✔
796
                        <button type="submit" class="button" <?php echo $writable ? '' : 'disabled'; ?>>
7 ✔
797
                            <?php esc_html_e('Switch to Development Mode', 'sentinel'); ?>
7 ✔
798
                        </button>
7 ✔
799
                    </p>
7 ✔
800
                </form>
7 ✔
801
            <?php else: ?>
7 ✔
802
                <p>
1 ✔
803
                    <?php esc_html_e('Switching back to production mode will remove the PRODUCTION define from wp-config.php (the runtime default is true). Developer-only UI will be hidden on the next page load.', 'sentinel'); ?>
1 ✔
804
                </p>
1 ✔
805
                <form method="post" action="">
1 ✔
806
                    <?php wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD); ?>
1 ✔
807
                    <input type="hidden" name="sentinel_unity_action" value="production_on">
1 ✔
808
                    <p>
1 ✔
809
                        <button type="submit" class="button button-primary" <?php echo $writable ? '' : 'disabled'; ?>>
1 ✔
810
                            <?php esc_html_e('Switch to Production Mode', 'sentinel'); ?>
1 ✔
811
                        </button>
1 ✔
812
                    </p>
1 ✔
813
                </form>
1 ✔
814
            <?php endif; ?>
1 ✔
815

816
            <!-- Manual instructions, always shown for reference -->
8 ✔
817
            <h2><?php esc_html_e('Manual Configuration', 'sentinel'); ?></h2>
8 ✔
818
            <p>
819
                <?php
820
                printf(
8 ✔
821
                    esc_html__('You can also manage the kill switch by editing %s directly. Add or update one of these lines before the %s comment:', 'sentinel'),
8 ✔
822
                    '<code>wp-config.php</code>',
8 ✔
823
                    "<code>/* That's all, stop editing! */</code>"
8 ✔
824
                );
8 ✔
825
                ?>
826
            </p>
8 ✔
827
            <pre style="padding:12px;background:#f6f7f7;border:1px solid #dcdcde;border-radius:3px;max-width:720px;"><code><?php echo esc_html(self::WP_CONFIG_MARKER); ?>
8 ✔
828
define( 'UNITY_KILL', true );  // Disable Unity
8 ✔
829
// define( 'UNITY_KILL', false ); // Or remove the line entirely to re-enable
8 ✔
830

831
<?php echo esc_html(self::PRODUCTION_MARKER); ?>
8 ✔
832
define( 'PRODUCTION', false ); // Development mode (expose developer UI)
8 ✔
833
// Remove the line entirely to return to production mode (the default)</code></pre>
8 ✔
834
        </div>
8 ✔
835
        <?php
8 ✔
836
    }
837
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc