Lines 100.00% 24 / 24
Methods 100.00% 3 / 3
Classes 100.00% 1 / 1
Covered by tests of size
Name Lines Methods CRAP
 redactQueryString 100.00% 12 / 12 100.00% 1 / 1 5
 redactUrl 100.00% 11 / 11 100.00% 1 / 1 3
 isSensitive 100.00% 1 / 1 100.00% 1 / 1 1
28final readonly class TrackingDataRedactor
29{
30    /** @var string Placeholder written in place of a sensitive value */
31    public const string REDACTED = '[redacted]';
32
33    /**
34     * Lowercase names of query parameters whose values must never be logged.
35     *
36     * @var list<string>
37     */
38    private const array SENSITIVE_PARAMETERS = [
39        'sig',
40        'signature',
41        'token',
42        'csrf',
43        'csrftoken',
44        'key',
45        'secret',
46        'password',
47        'passwd',
48        'pwd',
49        'pass',
50        'auth',
51        'apikey',
52        'api_key',
53        'access_token',
54    ];
55
56    /**
57     * Redacts the values of sensitive parameters in a raw query string while
58     * preserving parameter order and the values of non-sensitive parameters.
59     */
60    public function redactQueryString(string $queryString): string
61    {
62        if ($queryString === '') {
63            return '';
64        }
65
66        $pairs = explode('&', $queryString);
67        foreach ($pairs as $index => $pair) {
68            $separatorPosition = strpos($pair, needle: '=');
69            if ($separatorPosition === false) {
70                continue;
71            }
72
73            $name = substr($pair, offset: 0, length: $separatorPosition);
74            if (!$this->isSensitive($name)) {
75                continue;
76            }
77
78            $pairs[$index] = $name . '=' . self::REDACTED;
79        }
80
81        return implode('&', $pairs);
82    }
83
84    /**
85     * Redacts sensitive parameters in the query part of a URL (e.g. a referer),
86     * leaving the scheme, host, path and any fragment untouched.
87     */
88    public function redactUrl(string $url): string
89    {
90        $queryPosition = strpos($url, needle: '?');
91        if ($queryPosition === false) {
92            return $url;
93        }
94
95        $base = substr($url, offset: 0, length: $queryPosition + 1);
96        $query = substr($url, offset: $queryPosition + 1);
97
98        $fragment = '';
99        $fragmentPosition = strpos($query, needle: '#');
100        if ($fragmentPosition !== false) {
101            $fragment = substr($query, offset: $fragmentPosition);
102            $query = substr($query, offset: 0, length: $fragmentPosition);
103        }
104
105        return $base . $this->redactQueryString($query) . $fragment;
106    }
107
108    private function isSensitive(string $name): bool
109    {
110        return in_array(strtolower(urldecode($name)), self::SENSITIVE_PARAMETERS, strict: true);
111    }
112}