Обработка форм в 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;
loader
Комментарии
К этому посту больше нельзя оставлять новые комментарии
Логические задачи с собеседований