Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
94.44% covered (success)
94.44%
34 / 36
59.09% covered (warning)
59.09%
13 / 22
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Csrf
100.00% covered (success)
100.00%
39 / 39
94.44% covered (success)
94.44%
34 / 36
59.09% covered (warning)
59.09%
13 / 22
100.00% covered (success)
100.00%
10 / 10
47.39
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
 clear
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
 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
 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 Celema\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 = 'celema_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 clear(): void
49    {
50        $this->session->set($this->key, []);
51    }
52
53    public function verify(
54        string $page = 'default',
55        #[\SensitiveParameter]
56        ?string $token = null,
57    ): bool {
58        $token ??= $_POST[$this->field] ?? $_SERVER[$this->serverHeader()] ?? null;
59
60        if (!is_string($token)) {
61            return false;
62        }
63
64        if (hash_equals('', $token) || hash_equals('0', $token)) {
65            return false;
66        }
67
68        $savedToken = $this->tokens()[$page] ?? null;
69
70        if ($savedToken === null) {
71            return false;
72        }
73
74        return hash_equals($savedToken, $token);
75    }
76
77    protected function set(string $page = 'default'): string
78    {
79        $tokens = $this->tokens();
80        $token = base64_encode(random_bytes(32));
81        $tokens[$page] = $token;
82        $this->session->set($this->key, $tokens);
83
84        return $token;
85    }
86
87    private function initStorage(): void
88    {
89        if (!$this->session->has($this->key)) {
90            $this->session->set($this->key, []);
91        }
92    }
93
94    /** @return array<array-key, string> */
95    private function tokens(): array
96    {
97        /** @psalm-suppress MixedAssignment */
98        $tokens = $this->session->get($this->key, []);
99
100        if (!is_array($tokens)) {
101            return [];
102        }
103
104        $valid = [];
105
106        /** @psalm-suppress MixedAssignment */
107        foreach ($tokens as $page => $token) {
108            if (is_string($token)) {
109                $valid[$page] = $token;
110            }
111        }
112
113        return $valid;
114    }
115
116    /** @return non-empty-string */
117    private function serverHeader(): string
118    {
119        $header = strtoupper(strtr($this->header, '-', '_'));
120
121        if (str_starts_with($header, 'HTTP_')) {
122            return $header;
123        }
124
125        return 'HTTP_' . $header;
126    }
127}

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 = 'celema_csrf_tokens',
18        private readonly string $field = '_token',
19        private readonly string $header = 'X-CSRF-Token',
20    ) {
21        $this->initStorage();
22    }
Csrf->clear
50        $this->session->set($this->key, []);
51    }
Csrf->initStorage
89        if (!$this->session->has($this->key)) {
90            $this->session->set($this->key, []);
91        }
92    }
92    }
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->serverHeader
119        $header = strtoupper(strtr($this->header, '-', '_'));
119        $header = strtoupper(strtr($this->header, '-', '_'));
119        $header = strtoupper(strtr($this->header, '-', '_'));
119        $header = strtoupper(strtr($this->header, '-', '_'));
120
121        if (str_starts_with($header, 'HTTP_')) {
121        if (str_starts_with($header, 'HTTP_')) {
121        if (str_starts_with($header, 'HTTP_')) {
121        if (str_starts_with($header, 'HTTP_')) {
122            return $header;
125        return 'HTTP_' . $header;
126    }
Csrf->set
77    protected function set(string $page = 'default'): string
78    {
79        $tokens = $this->tokens();
80        $token = base64_encode(random_bytes(32));
81        $tokens[$page] = $token;
82        $this->session->set($this->key, $tokens);
83
84        return $token;
85    }
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
98        $tokens = $this->session->get($this->key, []);
99
100        if (!is_array($tokens)) {
101            return [];
104        $valid = [];
105
106        /** @psalm-suppress MixedAssignment */
107        foreach ($tokens as $page => $token) {
107        foreach ($tokens as $page => $token) {
107        foreach ($tokens as $page => $token) {
108            if (is_string($token)) {
107        foreach ($tokens as $page => $token) {
108            if (is_string($token)) {
109                $valid[$page] = $token;
107        foreach ($tokens as $page => $token) {
107        foreach ($tokens as $page => $token) {
108            if (is_string($token)) {
109                $valid[$page] = $token;
110            }
111        }
112
113        return $valid;
114    }
Csrf->verify
61            return false;
64        if (hash_equals('', $token) || hash_equals('0', $token)) {
64        if (hash_equals('', $token) || hash_equals('0', $token)) {
64        if (hash_equals('', $token) || hash_equals('0', $token)) {
65            return false;
68        $savedToken = $this->tokens()[$page] ?? null;
69
70        if ($savedToken === null) {
71            return false;
74        return hash_equals($savedToken, $token);
75    }