Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 2
CRAP
n/a
0 / 0
wpcom_build_fatal_error_signature
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
wpcom_decode_fatal_error_signature
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * Shared helper that builds a transportable fatal-error signature: a
4 * base64url-encoded JSON token over (kind, slug, version, wp, php) so
5 * consumers can group on the decoded parts. Reversible by design — the
6 * data is non-PII and already visible to admins on the fatal-error screen.
7 *
8 * @package automattic/jetpack-mu-wpcom
9 */
10
11if ( ! function_exists( 'wpcom_build_fatal_error_signature' ) ) {
12    /**
13     * Build a transportable fatal-error signature from extension metadata.
14     *
15     * Slug is lowercased and PHP_VERSION is reduced to MAJOR.MINOR.PATCH
16     * so distro suffixes don't fragment grouping. Returns null without an
17     * identifiable extension — a naked wp/php-only signature would absorb
18     * every unidentified fatal. Accepts canonical labels (plugin/muplugin/
19     * theme) or directory names (plugins/mu-plugins/themes) for `kind`.
20     *
21     * @param array $extension {
22     *     Required. Extension metadata.
23     *
24     *     @type string $kind    plugin|muplugin|theme (or plugins|mu-plugins|themes).
25     *     @type string $slug    Extension slug.
26     *     @type string $version Extension version string.
27     * }
28     * @return string|null Base64url-encoded JSON token, or null on bad input.
29     */
30    function wpcom_build_fatal_error_signature( $extension ) {
31        if ( ! is_array( $extension ) ) {
32            return null;
33        }
34
35        $kind_aliases = array(
36            'plugins'    => 'plugin',
37            'mu-plugins' => 'muplugin',
38            'themes'     => 'theme',
39            'plugin'     => 'plugin',
40            'muplugin'   => 'muplugin',
41            'theme'      => 'theme',
42        );
43        $raw_kind     = isset( $extension['kind'] ) ? (string) $extension['kind'] : '';
44        if ( ! isset( $kind_aliases[ $raw_kind ] ) ) {
45            return null;
46        }
47
48        $slug = isset( $extension['slug'] ) ? strtolower( trim( (string) $extension['slug'] ) ) : '';
49        if ( '' === $slug ) {
50            return null;
51        }
52
53        global $wp_version;
54
55        $parts = array(
56            'kind'    => $kind_aliases[ $raw_kind ],
57            'slug'    => $slug,
58            'version' => isset( $extension['version'] ) ? trim( (string) $extension['version'] ) : '',
59            'wp'      => isset( $wp_version ) ? trim( (string) $wp_version ) : '',
60            'php'     => sprintf( '%d.%d.%d', PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION ),
61        );
62
63        $json = wp_json_encode( $parts, JSON_UNESCAPED_SLASHES );
64        if ( false === $json ) {
65            return null;
66        }
67
68        // base64url: URL-safe alphabet, padding stripped.
69        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- transport encoding, not obfuscation.
70        return rtrim( strtr( base64_encode( $json ), '+/', '-_' ), '=' );
71    }
72}
73
74if ( ! function_exists( 'wpcom_decode_fatal_error_signature' ) ) {
75    /**
76     * Decode a fatal-error signature back to its parts array.
77     *
78     * @param string $token Base64url-encoded JSON token.
79     * @return array{kind:string,slug:string,version:string,wp:string,php:string}|null
80     *     Decoded parts, or null when the token is empty / malformed / wrong shape.
81     */
82    function wpcom_decode_fatal_error_signature( $token ) {
83        if ( ! is_string( $token ) || '' === $token ) {
84            return null;
85        }
86
87        // Reverse base64url: restore alphabet and re-pad for strict decode.
88        $std = strtr( $token, '-_', '+/' );
89        $pad = strlen( $std ) % 4;
90        if ( $pad > 0 ) {
91            $std .= str_repeat( '=', 4 - $pad );
92        }
93
94        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- transport encoding, not obfuscation.
95        $json = base64_decode( $std, true );
96        if ( false === $json ) {
97            return null;
98        }
99
100        $parts = json_decode( $json, true );
101        if ( ! is_array( $parts ) ) {
102            return null;
103        }
104
105        foreach ( array( 'kind', 'slug', 'version', 'wp', 'php' ) as $key ) {
106            if ( ! array_key_exists( $key, $parts ) || ! is_string( $parts[ $key ] ) ) {
107                return null;
108            }
109        }
110
111        return array(
112            'kind'    => $parts['kind'],
113            'slug'    => $parts['slug'],
114            'version' => $parts['version'],
115            'wp'      => $parts['wp'],
116            'php'     => $parts['php'],
117        );
118    }
119}