Ввод данных и их проверка

Фильтрация и проверка введенных пользователем данных, функции работы с информационными сообщениями и обработка исключений в Cotonti

#1. Фильтрация и проверка введенных пользователем данных

Для разработчика на PHP часто встает задача получения данных от пользователя получаемых через GET/POST запросы или из Cookie. Использование встроенных переменных $_GET, $_POST, $_COOKIE (и т.п.) на прямую, без какой либо фильтрации данных не безопасно и может привести к проблемам отображения страниц, потере данных и даже взлому системы. Именно поэтому внутри Cotonti везде используется специальная функция cot_import():

function cot_import($name, $source, $filter, $maxlen = 0, $dieonerror = false, $buffer = false)

Параметры функции:

  • $name - имя поля ввода данных в HTML форме.
  • $source - источник данных. Допустимые значения:
    • 'G' или 'GET' - для GET запросов;
    • 'P' или 'POST' - для POST данных;
    • 'C' или 'COOKIE' - для получения данных из cookie;
    • 'R' или 'REQUEST' - для импорта из PHP переменной $_REQUEST;
    • 'D' или 'DIRECT' - используется для фильтрации данных переданных в параметре $name;
    • 'PUT' - для импорта данных из HTTP PUT запроса (для REST сервисов);
    • 'DELETE' - для импорта данных из HTTP DELETE запроса (для REST);
    • 'PATCH' - для импорта данных из HTTP PATCH запроса (для REST).
  • $filter - имя фильтра, который будет использован для проверки данных. Доступные (встроенные) фильтры:
    • 'ALP' - фильтрует (удаляет) все символы, кроме символов латинского алфавита, цифр, дефиса и символа подчеркивания. Полезно использовать для ввода различных идентификаторов;
    • 'ARR' - импортирует данные как массив. Значения всех элементов массива должны быть дополнительно отфильтрованы разработчиком;
    • 'BOL' - импорт данных типа boolean, или чекбоксов. Возвращает TRUE, если чекбокс (флажок) выбран или получено значение 'on' или 1, FALSE если чекбокс не выбран или значение равно нулю, строке 'off'. Возвращает NULL в остальных случаях;
    • 'HTM' - пропускает любой текст обрезая ведущие и закрывающие пробелы, любые HTML теги и код будет оставлен без изменений;
    • 'INT' - пропускает только целые числовые значения, возвращает данные типа PHP (int). Возвращает NULL, если данне не являются целым числом;
    • 'NOC' - фильтр-пустышка (no check), пропускает любые данные без изменений;
    • 'NUM' - пропускает только числовые значения, возвращая переменную типа PHP (float) или NULL, если проверка не прошла;
    • 'PSW' - специальный фильтр для ввода паролей, удаляет символы кавычки, символы &, <, > и ограничивает строку до 32 знаков;
    • 'TXT' - фильтр пропускает обычный текст, но фильтрует HTML код, путем замены части спец символов на их текстовые представления;
  • $maxlen - задает максимальный размер импортируемых данных, 0 означает без ограничений;
  • $dieonerror - указывает скрипту остановить выполнение если импортируемые данные не прошли фильтр;
  • $buffer - включает использование буфера данных, который используется в API форм (Form API).

Таким образом, возвращаемое значение это фильтрованные данные или NULL в случае, если данные не прошли фильтр и пользователь должен ввести их повторно. Приведем несколько примеров:

$title  = cot_import('title',  'P', 'TXT');
$code   = cot_import('code',   'P', 'ALP');
$count  = cot_import('count',  'P', 'INT');
$text   = cot_import('text',   'P', 'HTM');
$notify = cot_import('notify', 'P', 'BOL');

 

#1.1. Дополнительные функции

После импорта значений надо их проверить на соответствие допустимым значениям (и в первую очередь определить не возвращено ли значение NULL, что означает, что введено не корректное значение или данные не получены из формы) и, при необходимости вывести сообщение об ошибке.

В наглядной форме это можно записать так:

if (is_null($count))
{
    cot_error('Допустимы только целые числа', 'count');
}

cot_error() стандартный метод дать понять системе, что что-то идет не так и вывести сообщение с ошибкой (сразу или в определенном месте). Функция принимает 2 параметра:

  • $message - текстовое сообщение или строка-метка (имя переменной в массиве локализованных данных). Например, если в подключенных языковых файлах определена переменная $L['my_err_msg'], мы можем использовать метку 'my_err_msg' и получим на выходе локализованное текстовое сообщение.
  • $src - (опционально) имя поля ввода в котором мы получили некорректные данные. Нужно указывать для отображения сообщений о ошибках рядом с полем ввода (зависит от настроек шаблонов и системной опции "Показывать сообщения отдельно для каждого источника").

Есть более короткий (рекомендуемый) вариант совместить проверку и вывод сообщения об ошибке:

cot_check(is_null($count), 'Допустимы только целые числа', 'count');

cot_check() использует следующие параметры:

  • $condition - значение булевского типа (boolean), например логическое выражение для проверки ошибки, которое в случае истины (TRUE) будет означать вывод ошибки;
  • $message - сообщение об ошибке (как в cot_error())для случаев, если условие $condition выполняется;
  • $src - (опционально) имя поля ввода с некорректными данными, аналогично cot_error().

Перед тем, как обрабатывать все полученные данные (например сохранять их в базу данных) надо проверить не случились ли какие-либо ошибки в системе, используя функцию cot_error_found():

if (!cot_error_found())
{
    // OK, мы можем сохранить полученные данные, т.к. ошибок импорта не возникло
}

 

При написании сложных расширений может потребоваться импорт целого списка параметров. Для таких случаев советуем обратить внимание на функцию cot_import_list(). Она позволяет импортировать параметры сразу списком:

// Пример 1: импортируем GET параметры (area, hash, link) 
// сразу в соответствующие по имени переменные
extract( cot_import_list('area, hash, link', 'G', null, null, 'ALP') );

// Пример 2: Список и соотв. фильтры можно задать массивом; 
// здесь импортируем POST параметры в массив, где ключи это имя параметра,
// а значения это данные переданные параметрами
$params = cot_import_list( array('id' => 'INT', 'c' => 'ALP', 'al' => 'TXT'));

 

Теперь вы знаете основы обработки данных в Котонти.

#2. Отображение сообщений

Теперь, когда вы умеете фильтровать данные и инициировать сообщения об ошибках, надо как-то их выводить их на странице.
Для этого будет достаточно двух строк —

  • первая в вашем шаблоне, которая подключает файл шаблона вывода уведомлений (в том месте где требуется выводить сами сообщения):
{FILE "{PHP.cfg.themes_dir}/{PHP.usr.theme}/warnings.tpl"}
  • вторая строка добавляется в основной код Расширения перед вызовом парсинга (компиляции) основного блока шаблона MAIN:
cot_display_messages($t);

Описанное выше предполагает, что:

  • $t это объект шаблона XTemplate;
  • вы разместили строку подключения шаблона сообщений warnings.tpl в основном блоке с именем MAIN.

Для иных случаев рассмотрим параметры cot_display_messages():

  • $tpl - объект XTemplate в котором должны быть выведены сообщения;
  • $block - полное имя блока, в котором находится директива подключения шаблона вывода уведомлений. Для примера: мы хотим разместить вывод сообщений в блоке 'RESULTS', который внутри блока 'MAIN', тогда значение параметра будет 'MAIN.RESULTS'.

#3. API сообщений

Иногда необходимо, кроме ошибок выводить информационные сообщения — например при успешном выполнении операции или выводить предупреждение о некритичных ошибках. Для этого в Cotonti есть messages API (программный интерфейс вывода сообщений).

Рассмотрим список функций этого API сообщений:

  • cot_check_messages()) - в основном используется внутри функций вывода сообщений;
  • cot_clear_messages()) - для очистки стека сообщений целиком или отдельных сообщений;
  • cot_die()) - немедленно завершает выполнение скрипта, если передано значение true;
  • cot_die_message()) - вывод стандартной страницы ошибок с возможностью переопределить текст сообщения;
  • cot_diefatal()) - завершает скрипт и выводит сообщение об ошибке (применяется в основном на этапе разработки);
  • cot_display_messages()) - используется для вывода сообщений внутри шаблона (см. следующую главу);
  • cot_get_message()) - возвращает массив строк с необработанными сообщениями, обычно используется функцией вывода;
  • cot_implode_messages()) - собирает все сообщения в единую строку, которую и возвращает;
  • cot_message()) - создает сообщение в системе.

Бо́льшая часть из списка это специальные функции и они вам вряд ли понадобятся при проектировании Расширений. Поэтому рассмотрим только основные.

cot_message()) аналогична функции cot_error(), но более общего назначения. Использует следующие параметры:

  • $text - текстовое сообщение или строка-метка (см. описание для cot_error());
  • $class - тип или CSS класс сообщения, допустимые значения: 'error', 'success', warning', 'ok';
  • $src - имя источника сообщения или имя поля ввода для вывода сообщения в определенном месте (при определенных условиях).

Используйте эту функцию для уведомления пользователя о статусе операции при заполнении форм и возврату на обычную страницу:

cot_check($some_input != '', 'some_err_msg', 'some_input');
if (!cot_error_found())
{
    // OK
    // Сохраняем данные...
    // ...
    // Выводим уведомление
    cot_message('Данные сохранены');
    // Возвращаемся на страницу редактирования
    cot_redirect(cot_url('mymod', 'm=edit'));
}

Используйте cot_die() в случае ошибки, когда дальнейшее выполнение скрипта должно быть прервано:

if ($something_really_wrong)
{
    // Пора завязывать с этим...
    cot_die();
}

Если же ошибка не критическая и подразумевает какой-то стандартный ответ от сервера (например запрошенная страница или ресурс не найдены), то лучше использовать специальные сообщения об ошибке, при загрузке которых браузеру возвращаются также соответствующие HTTP коды.
Для таких случаев применяйте функцию cot_die_message():

if ($page_not_found)
{
    // Выводим стандартную страницу 404-й ошибки с правильным HTTP кодом (404)
    cot_die_message(404);
}

Для нестандартных ошибок в cot_die_message() можно передавать произвольные заголовок и текст сообщения (третьим и четвертым параметром).

#4. Пример использования

Теперь, когда отдельные элементы описаны, посмотрим на пример кода реального Расширения:

// Подключаем языковой файл
require_once cot_langfile('mailout', 'plug');

if ($a == 'send' && $_SERVER['REQUEST_METHOD'] == 'POST')
{
    // Получение (фильтрация) данных из формы
    $subject = cot_import('subject', 'P', 'TXT');
    $limit   = cot_import('limit',   'P', 'INT');
    $content = cot_import('content', 'P', 'HTM');

    // Проверка введенных данных и формировании сообщений
    cot_check(empty($subject), 'mailout_err_subject_empty', 'subject');
    cot_check(empty($content), 'mailout_err_content_empty', 'content');
    cot_check($limit <= 0,     'mailout_err_invalid_limit', 'limit');

    if (!cot_error_found())
    {
        // Если ошибок нет — сохраняем данные
        $sql_limit = $limit > 0 ? "LIMIT $limit" : '';
        $res = $db->query("SELECT user_name, user_email FROM $db_users $sql_limit");
        $counter = 0;
        foreach ($res->fetchAll() as $row)
        {
            $to = '"' . addslashes($row['user_name']) . '" <' . $row['user_email'] . '>';
            cot_mail($to, $subject, $content, '', false, null, true);
            $counter++;
        }
        // Формируем сообщение об успешной операции
        cot_message("$counter messages sent.");
    }

    // Перенаправляем пользователя обратно на страницу редактироавния, где и будут отражены сообщения
    cot_redirect(cot_url('admin', 'm=other&p=mailout', '', true));
}

// Подготавливаем шаблон для интерфейса с формой 
$t = new XTemplate(cot_tplfile('mailout.tools', 'plug'));

// [Работаем с шаблоном — определяем теги и компилируем блоки] 

// Не забываем вывести сообщения в шаблон перед финальной обработкой
cot_display_messages($t);

// компиляция основного блока 
$t->parse('MAIN');

#5. Настраиваемые фильтры данных ввода

В первом разделе были рассмотрены встроенные в Cotonti варианты фильтров данных. Кроме перечисленных стандартных разработчик может создавать свои дополнительные типы фильтров.

Разберем на примере. Допустим у нас есть форма данных пользователя с полем ввода номера мобильного телефона, а нам надо проверять введенный номер на корректность (соответствие простым правилам).

Делается это достаточно просто, в 3 шага:

  1. Создаем функцию фильтрации, предполагая что будут вводится номера мобильных телефонов (из них нам нужен код мобильного оператора и номер):

    function mobilenum_filter($input_value, $name)
    {
       // выкидываем все кроме цифр
       $filtered = preg_replace("/[^\d]/", '', $input_value);
       // проверяем на количество знаков
       if (preg_match('/\d{10,12}/', $filtered)){
           // если номер содержит от 10 до 12 цифр считаем его годным
           // и возвращаем только последние 10 знаков (код оператора плюс номер)
           return substr($filtered, -10);
       } else {
           // если номер слишком короткий или длинный — данные фильтр не проходят
           return NULL;
       }
    }

    В функцию в качестве $input_value будет переданы «сырые» данные ввода пользователя. В $name будет передано имя поля ввода данных.

  2. Регистрируем функцию обработчик в специальном реестре фильтров:

    $cot_import_filters['MOB'][] = 'mobilenum_filter';
    
    • Переменная $cot_import_filters это и есть реестр. Она доступна в глобальной области видимости. Поэтому если обращаетесь к ней внутри функции на забывайте использовать ключевое слово global.
    • 'MOB' — имя фильтра может быть произвольным, если мы создаем новый фильтр или использовать предопределенный тип, если мы хотим переопределить стандартную функцию фильтрации фильтр, его мы используем далее.
    • 'mobilenum_filter' — имя регистрируемой функции обработчика
  3. Теперь используем новый фильтр при импорте данных:

    $mob_number = cot_import('mobnum', 'P', 'MOB');
    

    где 'mobnum' — это имя поля формы в который вводится телефонный номер, а 'MOB' это имя нового фильтра.

Для одного типа фильтра (на одно имя) может быть определено несколько функций обработчиков. Для фильтрации они будут вызваны последовательно. Если хотя бы один этап фильтрации не пройдет — ввод данных будет считаться некорректным.

#6. Защита данных формы

Если вы разработчик, то вероятно знаете, что данные на сервер можно передать различными способами. Большая часть отправляемых пользователем данных, вводится в формы и передается на сервер путем HTTP запросов. Наиболее используемые в этом случае типы запросов это GET и POST.

Как правило GET запросы используется для получения определенных данных (запрос нужного раздела сайта или конкретной страницы), а POST запросы для управления — передачи данных влияющих на изменение состояния системы (размещение новой страницы, удаление / изменение чего-либо). Поэтому критически важно контролировать такие запросы и быть уверенным, что запрос пришел из доверенного источника. Иными словами, что запрос сделал авторизованный пользователь с определенными правами.

Для проверки того факта, что введенные данные пришли от предполагаемого (авторизованного) пользователя, а не от злоумышленника, система использует специальную текстовую метку-код, которая передается вместе с данными при отправке формы POST запросом. Данная метка уникальна для каждого пользователя и для большей безопасности имеет ограниченный срок действия (устаревает).

Технически это реализовано следующим образом — в системе данная метка хранится в переменной $sys['xk'] и когда формируется страница с формой метка автоматически добавляется в форму с данными, в виде скрытого поля с именем x. При получении данных из формы метка проверяется на соответствие и актуальность. Если проверка не прошла, система сообщит об этом и предложит вернуться на предыдущую страницу и отправить данные еще раз. Таким образом защита POST запросов осуществляется прозрачно для пользователя и разработчика, и им не надо предпринимать каких-либо специальных действий.

Однако, если вам для каких-ибо целей, потребуется защитить данные передаваемые GET запросом, то вам нужно самостоятельно позаботится о включении проверочной метки в форму или ссылку.

Для добавления параметра безопасности в ссылку используется функция cot_xg(), которая возвращает строку видаx=ECBF66A246D. Пример использования:

…
'USERS_EDIT_SEND' => cot_url('users', 'm=edit&a=update&'.cot_xg().'&id='.$urr['user_id'])
…

Проверку метки в таком случае тоже нужно делать самостоятельно:

// получение данных ...

// проверка метки
// если проверка не пройдет, то выполнение скрипта будет остановлено с соответствующим сообщением
cot_check_xg(); 

// дальнейшая обработка данных

Отключение автоматического добавления метки

Для некоторых случаев, может потребоваться отключить автоматическое добавление метки безопасности. Например в случае, когда заполняемая форма предполагается к отправке на сторонний сервер. Для отключения метки достаточно указать у формы CSS класс xp-off.



1. Dr2005alex  20.03.2016 10:22

Респект!!! я оказывается не все видел)))) 

2. Roffun  20.03.2016 11:50

Очень полезная информация, чем больше таких статей в документации тем активнее будет приток новых пользователей.

p.s. В последнем примере описано:

"Для добавления параметра безопасности в ссылку используется функция" cot_xp() 

а в примере кода cot_xg()

'USERS_EDIT_SEND' => cot_url('users','m=edit&a=update&'.cot_xg().'&id='.$urr['user_id'])
3. Dr2005alex  20.03.2016 21:08

Да все-же опечатка, cot_xg() для добавления в url, а cot_xp() для добавления скрытого input со значением x в форму.

4. Macik  22.03.2016 23:43

Исправил. спасибо, что читаете доки!

5. CrazyFreeMan  21.07.2016 19:52

Хорошо бы расписать 

  • $src - (опционально) имя поля ввода в котором мы получили некорректные данные. Нужно указывать для отображения сообщений о ошибках рядом с полем ввода (зависит от настроек шаблонов и системной опции "Показывать сообщения отдельно для каждого источника").

Что именно должно быть в шаблоне, что это относится к ресурсам и что нужно добавить параметр $error а то важная мелочь :) 

6. Macik  10.09.2016 20:42

 CrazyFreeManсейчас в ядре этого нет. Т.е. сделать такой вывод можно только написав свой обработчик. `$src` используется системой для разделения ошибок внутри соответствующего массива. 

Добавление комментариев доступно только зарегистрированным пользователям