Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
94.87% covered (success)
94.87%
37 / 39
60.87% covered (warning)
60.87%
14 / 23
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Csrf
100.00% covered (success)
100.00%
41 / 41
94.87% covered (success)
94.87%
37 / 39
60.87% covered (warning)
60.87%
14 / 23
100.00% covered (success)
100.00%
10 / 10
47.42
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 token
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 refresh
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remove
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 verify
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
8 / 8
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
5
 set
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 savedToken
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 initStorage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 tokens
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
8 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 serverHeader
100.00% covered (success)
100.00%
4 / 4
77.78% covered (warning)
77.78%
7 / 9
25.00% covered (danger)
25.00%
2 / 8
100.00% covered (success)
100.00%
1 / 1
3.69
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Session;
6
7/** @api */
8class Csrf
9{
10    /**
11     * @param non-empty-string $key
12     * @param non-empty-string $field
13     * @param non-empty-string $header
14     */
15    public function __construct(
16        private readonly Session $session,
17        private readonly string $key = 'celemas_csrf_tokens',
18        private readonly string $field = '_token',
19        private readonly string $header = 'X-CSRF-Token',
20    ) {
21        $this->initStorage();
22    }
23
24    public function token(string $page = 'default'): string
25    {
26        $tokens = $this->tokens();
27        $token = $tokens[$page] ?? null;
28
29        if (is_string($token)) {
30            return $token;
31        }
32
33        return $this->set($page);
34    }
35
36    public function refresh(string $page = 'default'): string
37    {
38        return $this->set($page);
39    }
40
41    public function remove(string $page = 'default'): void
42    {
43        $tokens = $this->tokens();
44        unset($tokens[$page]);
45        $this->session->set($this->key, $tokens);
46    }
47
48    public function verify(
49        string $page = 'default',
50        #[\SensitiveParameter]
51        ?string $token = null,
52    ): bool {
53        $token ??= $_POST[$this->field] ?? $_SERVER[$this->serverHeader()] ?? null;
54
55        if (!is_string($token)) {
56            return false;
57        }
58
59        if (hash_equals('', $token) || hash_equals('0', $token)) {
60            return false;
61        }
62
63        $savedToken = $this->savedToken($page);
64
65        if ($savedToken === null) {
66            return false;
67        }
68
69        return hash_equals($savedToken, $token);
70    }
71
72    protected function set(string $page = 'default'): string
73    {
74        $tokens = $this->tokens();
75        $token = base64_encode(random_bytes(32));
76        $tokens[$page] = $token;
77        $this->session->set($this->key, $tokens);
78
79        return $token;
80    }
81
82    private function savedToken(string $page): ?string
83    {
84        $tokens = $this->tokens();
85        $token = $tokens[$page] ?? null;
86
87        return is_string($token) ? $token : null;
88    }
89
90    private function initStorage(): void
91    {
92        if (!$this->session->has($this->key)) {
93            $this->session->set($this->key, []);
94        }
95    }
96
97    /** @return array<array-key, string> */
98    private function tokens(): array
99    {
100        /** @psalm-suppress MixedAssignment */
101        $tokens = $this->session->get($this->key, []);
102
103        if (!is_array($tokens)) {
104            return [];
105        }
106
107        $valid = [];
108
109        /** @psalm-suppress MixedAssignment */
110        foreach ($tokens as $page => $token) {
111            if (is_string($token)) {
112                $valid[$page] = $token;
113            }
114        }
115
116        return $valid;
117    }
118
119    /** @return non-empty-string */
120    private function serverHeader(): string
121    {
122        $header = strtoupper(strtr($this->header, '-', '_'));
123
124        if (str_starts_with($header, 'HTTP_')) {
125            return $header;
126        }
127
128        return 'HTTP_' . $header;
129    }
130}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Csrf->__construct
16        private readonly Session $session,
17        private readonly string $key = 'celemas_csrf_tokens',
18        private readonly string $field = '_token',
19        private readonly string $header = 'X-CSRF-Token',
20    ) {
21        $this->initStorage();
22    }
Csrf->initStorage
92        if (!$this->session->has($this->key)) {
93            $this->session->set($this->key, []);
94        }
95    }
95    }
Csrf->refresh
36    public function refresh(string $page = 'default'): string
37    {
38        return $this->set($page);
39    }
Csrf->remove
41    public function remove(string $page = 'default'): void
42    {
43        $tokens = $this->tokens();
44        unset($tokens[$page]);
45        $this->session->set($this->key, $tokens);
46    }
Csrf->savedToken
82    private function savedToken(string $page): ?string
83    {
84        $tokens = $this->tokens();
85        $token = $tokens[$page] ?? null;
86
87        return is_string($token) ? $token : null;
87        return is_string($token) ? $token : null;
87        return is_string($token) ? $token : null;
87        return is_string($token) ? $token : null;
88    }
Csrf->serverHeader
122        $header = strtoupper(strtr($this->header, '-', '_'));
122        $header = strtoupper(strtr($this->header, '-', '_'));
122        $header = strtoupper(strtr($this->header, '-', '_'));
122        $header = strtoupper(strtr($this->header, '-', '_'));
123
124        if (str_starts_with($header, 'HTTP_')) {
124        if (str_starts_with($header, 'HTTP_')) {
124        if (str_starts_with($header, 'HTTP_')) {
124        if (str_starts_with($header, 'HTTP_')) {
125            return $header;
128        return 'HTTP_' . $header;
129    }
Csrf->set
72    protected function set(string $page = 'default'): string
73    {
74        $tokens = $this->tokens();
75        $token = base64_encode(random_bytes(32));
76        $tokens[$page] = $token;
77        $this->session->set($this->key, $tokens);
78
79        return $token;
80    }
Csrf->token
24    public function token(string $page = 'default'): string
25    {
26        $tokens = $this->tokens();
27        $token = $tokens[$page] ?? null;
28
29        if (is_string($token)) {
30            return $token;
33        return $this->set($page);
34    }
Csrf->tokens
101        $tokens = $this->session->get($this->key, []);
102
103        if (!is_array($tokens)) {
104            return [];
107        $valid = [];
108
109        /** @psalm-suppress MixedAssignment */
110        foreach ($tokens as $page => $token) {
110        foreach ($tokens as $page => $token) {
110        foreach ($tokens as $page => $token) {
111            if (is_string($token)) {
110        foreach ($tokens as $page => $token) {
111            if (is_string($token)) {
112                $valid[$page] = $token;
110        foreach ($tokens as $page => $token) {
110        foreach ($tokens as $page => $token) {
111            if (is_string($token)) {
112                $valid[$page] = $token;
113            }
114        }
115
116        return $valid;
117    }
Csrf->verify
56            return false;
59        if (hash_equals('', $token) || hash_equals('0', $token)) {
59        if (hash_equals('', $token) || hash_equals('0', $token)) {
59        if (hash_equals('', $token) || hash_equals('0', $token)) {
60            return false;
63        $savedToken = $this->savedToken($page);
64
65        if ($savedToken === null) {
66            return false;
69        return hash_equals($savedToken, $token);
70    }