Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.43% covered (warning)
71.43%
25 / 35
66.67% covered (warning)
66.67%
2 / 3
CRAP
n/a
0 / 0
pcg_confirm_maybe_register_hook
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
pcg_confirm_validate_request
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
pcg_confirm_inject_active_plugins
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Early-bootstrap hook for the PCG confirmation probe. Must load at
4 * mu-plugin time — `pre_option_active_plugins` has to be registered
5 * before wp-settings.php reads `active_plugins`.
6 *
7 * @package automattic/jetpack-mu-wpcom
8 */
9
10pcg_confirm_maybe_register_hook();
11
12/**
13 * Validate the request and register the `pre_option_active_plugins`
14 * filter when this is a valid confirmation probe.
15 */
16function pcg_confirm_maybe_register_hook() {
17    // Bail if `plugins_loaded` already fired — we're loaded too late for
18    // the active-plugin injection to take effect (dev path where
19    // jetpack-mu-wpcom runs as a regular plugin, not a mu-plugin).
20    if ( did_action( 'plugins_loaded' ) ) {
21        return;
22    }
23
24    // phpcs:disable WordPress.Security.NonceVerification.Recommended -- token (validated by `pcg_confirm_validate_request`) is the nonce.
25    $plugin_mains = pcg_confirm_validate_request( $_GET );
26    // phpcs:enable WordPress.Security.NonceVerification.Recommended
27    if ( empty( $plugin_mains ) ) {
28        return;
29    }
30
31    // Only single-site active_plugins is hooked; pre_option_active_sitewide_plugins
32    // would need the same treatment if PCG ever guards network activations.
33    add_filter(
34        'pre_option_active_plugins',
35        static fn( $value ) => pcg_confirm_inject_active_plugins( $value, $plugin_mains )
36    );
37}
38
39/**
40 * Pure validation: returns the plugin-main list iff $request is a valid
41 * confirmation probe (correct flags, well-formed token, matching
42 * transient with `confirm === true`, non-empty plugins). Empty array
43 * otherwise. Extracted from the entry hook for unit testability.
44 *
45 * @internal
46 * @param array $request Request array (typically `$_GET`).
47 * @return string[] Plugin main absolute paths, or empty on any failure.
48 */
49function pcg_confirm_validate_request( array $request ) {
50    $probe_flag   = sanitize_text_field( wp_unslash( $request['pcg_probe'] ?? '' ) );
51    $confirm_flag = sanitize_text_field( wp_unslash( $request['pcg_confirm'] ?? '' ) );
52    $raw_token    = sanitize_text_field( wp_unslash( $request['token'] ?? '' ) );
53    if ( '1' !== $probe_flag || '1' !== $confirm_flag ) {
54        return array();
55    }
56    // Length is locked to what wp_generate_password( 32, false ) emits;
57    // the transient lookup is still the real gate, but anchoring length
58    // here rules out truncation / accidental-collision noise.
59    $token = preg_match( '/^[A-Za-z0-9]{32}$/', $raw_token ) ? $raw_token : '';
60    if ( '' === $token ) {
61        return array();
62    }
63
64    require_once __DIR__ . '/class-pcg-load-tester.php';
65    $payload = get_transient( PCG_Load_Tester::transient_key( $token ) );
66    // During a mixed-version deploy a pre-deploy transient may lack the
67    // `confirm` key; strict `true ===` keeps that case on the safe side
68    // (no injection — the probe endpoint will manually require as before).
69    if ( ! is_array( $payload ) || true !== ( $payload['confirm'] ?? false ) ) {
70        return array();
71    }
72    return is_array( $payload['plugins'] ?? null ) ? array_values(
73        array_filter(
74            array_map( static fn( $p ) => (string) $p, $payload['plugins'] ),
75            static fn( $p ) => '' !== $p
76        )
77    ) : array();
78}
79
80/**
81 * Merge confirmation-probe candidates into the `active_plugins` option
82 * value. When $value is false, read the cached option without going
83 * through `get_option()` — `get_option()` re-fires the same
84 * `pre_option_active_plugins` filter, which would recurse forever.
85 *
86 * @internal Exposed for unit tests.
87 * @param mixed    $value         Existing filter value (false = "let WP read the option").
88 * @param string[] $plugin_mains  Absolute paths to candidate plugin main files.
89 * @return string[] Merged active_plugins list of basenames.
90 */
91function pcg_confirm_inject_active_plugins( $value, array $plugin_mains ) {
92    if ( false === $value ) {
93        $alloptions = wp_load_alloptions();
94        $raw        = $alloptions['active_plugins'] ?? array();
95        $existing   = is_array( $raw ) ? $raw : (array) maybe_unserialize( $raw );
96    } else {
97        $existing = (array) $value;
98    }
99    $basenames = array_map( 'plugin_basename', $plugin_mains );
100    return array_values( array_unique( array_merge( $existing, $basenames ) ) );
101}