Обработка форм в PHP
В этом уроке мы рассмотрим примеры безопасной обработки запросов в PHP.
Общие сценарии с обработкой GET- и POST-запросов мы рассмотрели в соответствующих уроках курса PHP для начинающих. Однако, несмотря на приведенные в уроках примеры, многие начинающие продолжают делать ошибки, никак не проверяя данные, пришедшие с веб-формы. Поэтому я решил сделать отдельный урок по этой теме, чтобы было понимание, как делать можно, а как - нельзя.
Давайте в качестве примера рассмотрим одно из домашних заданий, выполненных учеником к уроку Калькулятор на PHP.
Вот непосредственно форма для отправки запроса:
<html>
<head>
<title>Калькулятор</title>
</head>
<body>
<form action="/result.php">
<input type="text" name="x1">
<select name="operation">
<option value="+">+</option>
<option value="-">-</option>
</select>
<input type="text" name="x2">
<input type="submit" value="Посчитать">
</form>
</body>
</html>
Вот скрипт result.php, принимающий запрос:
<?php
$result = require __DIR__ . '/calc.php';
?>
<html>
<head>
<title>Калькулятор</title>
</head>
<body>
<b>Результат вычислений:</b>
<br>
<?= $result ?>
</body>
</html>
А вот скрипт calc.php, в котором происходит обработка запроса:
<?php
if (empty($_GET)) {
return 'Ничего не передано';
}
if (empty($_GET['operation'])) {
return 'Операция не передана';
}
if ($_GET['x1'] === '' || $_GET['x2'] === '') {
return 'Аргументы 1 или 2 не переданы';
}
$x1 = $_GET['x1'];
$x2 = $_GET['x2'];
$operations = $_GET['operation'];
if (is_numeric($x1) && is_numeric($x2)) {
switch ($operations) {
case '+':
$result = $x1 + $x2;
break;
case '-':
$result = $x1 - $x2;
break;
case '/':
$result = $x2 != 0 ? ($x1 / $x2) : 'На ноль делить нельзя';
break;
case '*':
$result = $x1 * $x2;
break;
default:
return 'Операция не поддерживается';
}
} else {
return 'Введите число';
}
$expression = $x1 . ' ' . $operations . ' ' . $x2 . ' = ';
return $expression . $result;
Давайте попробуем зайти на страничку с формой и отправить несколько запросов. Заполняем форму.
После чего жмём кнопку "Посчитать".
На первый взгляд всё работает. Давайте проверим как работает валидация того, что поле заполнено. Оставляем один из аргументов пустым.
Отлично! Всё прекрасно обработалось.
Давайте теперь в качестве аргумента попробуем передать не числовое значение.
Казалось бы, скрипт всё корректно обрабатывает. Однако это не так. Отсутствует проверка того, что в массиве $_GET вообще присутствует ключ x1 и x2. Если мы сейчас напрямую в адресной строке на странице с результатом:
http://myproject.loc/result.php?x1=1&operation=%2B&x2=2
удалим хотя бы один аргумент:
http://myproject.loc/result.php?x1=%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B0&operation=%2B
Если в данный момент ошибки у вас не отобразились, нужно в php.ini вашего development-окружения задать следующие директивы:
display_errors = On;
error_reporting = E_ALL;
После чего перезапустить веб-сервер.
В этих ошибках говорится о том, что ключа x1 в массиве нет. И действительно, откуда ему там взяться. Если вы сейчас подумали: "Ну, чтобы в адресной строке не писали всякую ерунду, можно перейти на POST-запросы.", спешу вас разочаровать. Любое поле на форме можно изменить прямо в браузере и отправить абсолютно любой запрос, какой только взбредет в голову. Для этого есть консоль разработчика.
Вот поле есть. Нажимаем delete. И вуаля, форма уже косячная.
И если вы думаете, что никто так делать не будет, то вы ошибаетесь. Как только вы выложите сайт в интернет, его будут пытаться взломать и будут слать в запросах абсолютно дичайшую дичь.
Что же нам делать? Нам в самом простом случае просто требуется проверить, что такое поле в массиве $_GET есть. Сделать это можно с помощью empty:
if (empty($_GET['x1']) || empty($_GET['x2'])) {
return 'Аргументы 1 или 2 не переданы';
}
Однако, если передать 0 в качестве одного из аргументов, наш калькулятор поведет себя некорректно:
Всё потому, что empty(0) вернёт true. Вместо этого можно воспользоваться конструкцией isset:
if (!isset($_GET['x1'], $_GET['x2'])) {
return 'Аргументы 1 или 2 не переданы';
}
В таком случае всё будет корректно валидироваться.
И если удалим один из аргументов с формы, то получим законную ошибку:
http://myproject.loc/result.php?operation=%2B&x2=1
С аналогичным успехом можно было воспользоваться функцией array_key_exists:
if (!array_key_exists('x1', $_GET) || !array_key_exists('x2', $_GET)) {
return 'Аргументы 1 или 2 не переданы';
}
С той лишь разницей, что если в массиве по нужному нам ключу будет null, то isset для него вернёт false. То есть разница в следующем:
$foo = [];
$foo['bar'] = null;
var_dump(isset($foo['bar'])); // false
var_dump(array_key_exists('bar', $foo)); // true
Это нужно учитывать, если в запросе в качестве валидного значения может прилететь null.
А ещё в PHP 7 появился null-coalesce оператор, который очень удобно использовать для наших целей:
$x1 = $_GET['x1'] ?? null;
$x2 = $_GET['x2'] ?? null;
if ($x1 === null || $x2 === null) {
return 'Аргументы 1 или 2 не переданы';
}
И далее по коду уже работать с переменными $x1 и $x2.
После того, как мы проверили существование ключа, нужно проверить что значение по этому ключу является валидным. В нашем случае требуется проверить, что x1 и x2 - числа. Для этого можно использовать функцию is_numeric:
if (!is_numeric($x1) || !is_numeric($x2)) {
return 'Введите число';
}
А дальше, если уж всё совсем делать правильно, требуется привести значения переменных к правильному типу, ведь значения, пришедшие с формы всегда строковые.
$x1 = (float)$x1;
$x2 = (float)$x2;
И только после этого можно приступать к работе с этими значениями.
В PHP также есть специальные функции, позволяющие упростить обработку входных значений. Одна из таких функций - filter_input. Ознакомиться с ней я предлагаю вам самостоятельно в рамках домашнего задания к этому уроку. В качестве самостоятельной работы предлагаю вам переписать код обработчика, приведенного ниже, с использованием этой функции:
<?php
if (empty($_GET)) {
return 'Ничего не передано';
}
if (empty($_GET['operation'])) {
return 'Операция не передана';
}
$x1 = $_GET['x1'] ?? null;
$x2 = $_GET['x2'] ?? null;
if ($x1 === null || $x2 === null) {
return 'Аргументы 1 или 2 не переданы';
}
$operations = $_GET['operation'];
if (!is_numeric($x1) || !is_numeric($x2)) {
return 'Введите число';
}
$x1 = (float)$x1;
$x2 = (float)$x2;
switch ($operations) {
case '+':
$result = $x1 + $x2;
break;
case '-':
$result = $x1 - $x2;
break;
case '/':
$result = $x2 !== 0 ? ($x1 / $x2) : 'На ноль делить нельзя';
break;
case '*':
$result = $x1 * $x2;
break;
default:
return 'Операция не поддерживается';
}
$expression = $x1 . ' ' . $operations . ' ' . $x2 . ' = ';
return $expression . $result;
Комментарии