Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.56% covered (success)
97.56%
40 / 41
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Csrf
97.56% covered (success)
97.56%
40 / 41
66.67% covered (warning)
66.67%
2 / 3
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 createNewToken
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 setValidator
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
8
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Form\Element\Input;
16
17/**
18 * Form CSRF element class
19 *
20 * @category   Pop
21 * @package    Pop\Form
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27
28class Csrf extends Hidden
29{
30
31    /**
32     * Current token data
33     * @var array
34     */
35    protected array $token = [];
36
37    /**
38     * Constructor
39     *
40     * Instantiate the CSRF input form element
41     *
42     * @param  string  $name
43     * @param  ?string $value
44     * @param  int     $expire
45     * @param  ?string $indent
46     */
47    public function __construct(string $name, ?string $value = null, int $expire = 300, ?string $indent = null)
48    {
49        // Start a session.
50        if (session_id() == '') {
51            session_start();
52        }
53
54        $this->setName($name);
55
56        // If a token does not exist for this field name, create one
57        if (!isset($_SESSION['pop_csrf'][$name])) {
58            $this->createNewToken($value, $expire);
59        // Else, retrieve existing token
60        } else {
61            $this->token = $_SESSION['pop_csrf'][$name];
62
63            // Check to see if the token has expired
64            if ($this->token['expire'] > 0) {
65                if (($this->token['expire'] + $this->token['start']) < time()) {
66                    $this->createNewToken($value, $expire);
67                }
68            }
69        }
70
71        parent::__construct($name, $this->token['value'], $indent);
72        $this->setRequired(true);
73        $this->setValidator();
74    }
75
76    /**
77     * Set the token of the csrf form element
78     *
79     * Tokens are namespaced in the session by field name, so multiple CSRF-protected
80     * forms (or fields) can coexist in the same session without clobbering each other.
81     *
82     * @param  ?string $value
83     * @param  int     $expire
84     * @return Csrf
85     */
86    public function createNewToken(?string $value = null, int $expire = 300): Csrf
87    {
88        $this->token = [
89            'value'  => $value ?? bin2hex(random_bytes(32)),
90            'expire' => (int)$expire,
91            'start'  => time()
92        ];
93
94        if (!isset($_SESSION['pop_csrf']) || !is_array($_SESSION['pop_csrf'])) {
95            $_SESSION['pop_csrf'] = [];
96        }
97        $_SESSION['pop_csrf'][$this->name] = $this->token;
98
99        return $this;
100    }
101
102    /**
103     * Set the validator
104     *
105     * @throws Exception
106     * @return void
107     */
108    protected function setValidator(): void
109    {
110        // Get query data
111        if (!isset($_SERVER['REQUEST_METHOD'])) {
112            throw new Exception('Error: The server request method is not set.');
113        }
114
115        $queryData = [];
116        switch ($_SERVER['REQUEST_METHOD']) {
117            case 'GET':
118                $queryData = $_GET;
119                break;
120
121            case 'POST':
122                $queryData = $_POST;
123                break;
124
125            default:
126                $input = fopen('php://input', 'r');
127                $qData = '';
128                while ($data = fread($input, 1024)) {
129                    $qData .= $data;
130                }
131
132                parse_str($qData, $queryData);
133        }
134
135        // If there is query data, set validator to check the submitted value against the
136        // real, server-side token value using a timing-safe comparison.
137        if (count($queryData) > 0) {
138            $expectedValue = $this->token['value'] ?? '';
139            $this->addValidator(function ($value) use ($expectedValue) {
140                return hash_equals($expectedValue, (string)$value) ? null : 'The security token does not match.';
141            });
142        }
143    }
144
145}