Джино: хостинг и веб-сервисы

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | HTML_MetaForm: извлечение информации о структуре HTML-формы и ее обработка 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


2006-09-24
Обсудить на форуме

Одно из главных отличий Web-программирования от программирования оконных приложений (GUI) заключается в том, как приложение принимает и обрабатывает вводимые пользователем данные.

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

Библиотека HTML_MetaForm, а также сопутствующая библиотека HTML_MetaFormAction позволяют свести данные различия к минимуму, не изменяя при этом традиционную структуру Web-скриптов. Вот полный список возможностей комплекса:

  • Автоматическое извлечение полной информации о структуре формы (метаданные) прямо из HTML-потока с последующей передачей ее обработчику формы.
  • Цифровое подписывание формы: обработчик может быть уверен, что полученные метаданные достоверны, а не подделаны злоумышленником. Обеспечение соответствий: обработчика атрибуту action формы, выбранного значения элементу выпадающего списка, допустимости текста скрытого поля.
  • Сохранение сведений о структуре (тексты и значения) выпадающих списков, radio-кнопок и checkbox-ов.
  • Передача в обработчик различных мета-атрибутов, задаваемых прямо в HTML-коде формы (например, сведения о валидаторах).
  • Накопление сообщений об ошибках валидации, привязанных прямо к полям формы, с возможностью последующего отображения в настраиваемом дизайне. При этом данные, введенные в поля ранее, автоматически остаются на своих местах.
  • Автоматический (возможен и ручной) запуск валидаторов, привязанных прямо к HTML-полям формы.
  • Запуск реакции на ту или иную кнопку формы, если ошибок валидации не было.

Метаданные формы

Рассмотрим пример простой POST-формы, состоящей всего из трех элементов: текстового поля, выпадающего списка и кнопки отправки формы.

Листинг 1
<?php ## t_metainfo_draw.php: draw the form with attached metadata.
include_once "../../lib/config.php";
include_once "../../HTML_FormPersister/lib/config.php";
include_once "HTML/MetaForm.php"; 
// Create new MetaForm object (processor).
$metaForm =& new HTML_MetaForm('secret_digital_signature_YS0lTgit');
// Parse HTML output & extract form meta-information.
ob_start(array(&$metaForm, 'process'));
?>
<form action="t_meta_process.php" method="POST">
  <label for="t">Anything</label>:
  <input type="text" name="test" id="t" meta:validator="filled"><br>
  Select: 
  <select name="sel">
    <option value="a">aaa</option>
    <option value="b">bbb</option>
  </select><br>
  <input type="submit" name="doSend" value="Send!">
</form>

За счет того, что мы назначили обработчик HTML_MetaForm для выходного HTML-потока, скрипт приема формы t_meta_process.php получит не только POST-данные формы, но также и полную информацию о ее структуре:

Листинг 2
<?php ## t_meta_process.php: process metadata attached to form.
include_once "../../lib/config.php";
include_once "../../HTML_FormPersister/lib/config.php";
include_once "HTML/MetaForm.php"; 
$metaForm =& new HTML_MetaForm('secret_digital_signature_YS0lTgit');
echo "<pre>";
print_r($metaForm->getFormMeta()); 
echo "</pre>";
?>

В структуре этих метаданных легко разобраться, если внимательно взглянуть на результаты выполнения обработчика (для удобства некоторые наиболее интересные строки пронумерованы слева):

Листинг 3
Array
(
1   [original] => t_meta_process.php
    [name] => 
    [type] => form
    [id] => 
    [items] => Array
        (
            [test] => Array
                (
2                   [validator] => filled
                    [type] => text
                    [id] => t
                    [name] => test
3                   [label] => Anything
                    [value] => any text
                )

            [sel] => Array
                (
                    [type] => single
4                   [items] => Array
                        (
                            [a] => aaa
                            [b] => bbb
                        )

                    [name] => sel
5                   [value] => a
                )

            [doSend] => Array
                (
                    [type] => action
                    [name] => doSend
                    [value] => Отправить!
                )

        )
    ...
)

Таким образом, имея традиционную форму и обычный обработчик, добавив 2 строчки в каждый из них, мы получили:

  1. Гарантию, что для формы запущен именно тот обработчик, на который рассчитывал скрипт генерации (строка (1) содержит значение атрибута action формы, которое сравнивается с REQUEST_URI).
  2. Возможность задать некоторые мета-атрибуты для любого поля формы (обратите внимание на предложение meta:validator в первом листинге, а также на строку (2) результатов) и получать их из скрипта-обработчика. Также для поля формы сохраняются его имя и атрибут id.
  3. Если для элемента формы задана текстовая метка (label), в атрибуте label она будет указана (3). Это крайне удобно для вывода диагностических сообщений ошибок валидации.
  4. Сведения об элементах выпадающего списка sel (4) (в том числе — список всех допустимых значений данного элемента).
  5. Дополнительно проставляется также значение, введенное или выбранное пользователем в данном элементе формы (5).

Как это работает?

Чтобы не вдаваться в детали алгоритма, сообщу лишь следующее: после того, как форма будет проанализирована библиотекой и составятся ее полные метаданные, они упаковываются и сохраняются в hidden-поле с именем HTML_MetaForm (по умолчанию), автоматически вставляемом непосредственно после тэга <form>. Данные подписываются специальной цифровой подписью, известной только скрипту (по умолчанию она равна времени изменения файла библиотеки, но может быть задана вручную в конструкторе HTML_MetaForm). Обработчик формы распаковывает данное поле, проверяет его цифровую подпись (чтобы исключить возможность подделки метаданных) и возвращает метаданные в программу.

Подробности вы можете узнать, проанализировав PHP-код библиотеки и имеющиеся в нем комментарии.

Типы полей формы

С точки зрения обработчика существуют следующие типы элементов метаданных:

  • text: обычное поле, содержащее текстовое значение. В HTML это: <input type="text">, <input type="hidden"> и <textarea>.
  • single: элемент с единичным выбором. В HTML это: <select>, а также группа radio-кнопок с одинаковым именем (функционально они полностью идентичны).
  • multiple: элемент с множественным выбором. В HTML это: <select multiple>, а также группа checkbox-ов с одинаковым именем (функционально они полностью идентичны). Очень важно, чтобы в конце имени таких элементов стояла пара квадратных скобок [], иначе они не будут работать (особенность PHP).
  • action: кнопка действия, при нажатии на которую форма отправляется обработчику.
  • file: поле выбора файла для закачки (функционально отличается от поля text только тем, что атрибут value содержит ассоциативный массив с информацией о закачанном файле).
  • flag: checkbox с отдельным именем, расположенный вне какой-либо группы. Отличается тем, что его значение всегда равно true или false.

Мета-атрибуты

В примере выше мы использовали мета-атрибут meta:validator, чтобы указать область допустимых значений для поля формы. При помощи других мета-атрибутов (префикс meta:) можно хранить информацию, привязанную к полям формы, прямо в самой форме, где она всегда под рукой и легко расширяется. Например, если то или иное поле должно быть вставлено в столбец таблицы базы данных, можно указать мета-атрибут meta:col, в котором хранится имя этого столбца. Скрипт-обработчик может извлечь данные сведения и на их основе построить SQL-запрос вставки.

Набор мета-атрибутов ограничен лишь фантазией программиста.

Обработка формы

Сохраняемые метаданные необходимы при обработке формы, которую облегчает библиотека HTML_MetaFormAction. Она проводит все необходимые проверки цифровых подписей, сверку введенных пользователем значений с допустимыми вариантами, а также запуск привязанных к полям валидаторов. Сообщения о произошедших ошибках накапливаются; при этом сохраняется, к какому элементу формы привязана та или иная ошибка (указываются значения атрибутов name и id). Проще всего это понять на следующем далее примере.

Чайник 

В данном случае произведено объединение скрипта, отображающего форму, со скриптом, ее обрабатывающим. Это можно считать достаточно распространенной и удобной практикой. С использованием HTML_MetaForm и сопутствующих библиотек этот скрипт очень похож на программу, работающую в режиме GUI.

Листинг 4
<?php ## t_action.php: process simple action.
include_once "../../lib/config.php";
include_once "../../HTML_FormPersister/lib/config.php";
include_once "HTML/MetaForm.php"; 
include_once "HTML/MetaFormAction.php"; 

// Assign output processor to pass metadata.
$metaForm =& new HTML_MetaForm('secret_digital_signature_YS0lTgit');
ob_start(array(&$metaForm, 'process'));

// Now process the form (if posted).
$metaFormAction =& new HTML_MetaFormAction($metaForm);
switch ($metaFormAction->process()) {
    case 'INIT':
        // Called when script is called via GET method.
        // No buttons are pressed yet, initialize form fields.
        break;
        
    case 'doSend':
        // Called when doSend is pressed and THERE ARE 
        // NO VALIDATION ERRORS! Process the form.
        break;
}
?>

<form method="POST">
  <label for="t">Anything</label>:
  <input type="text" name="test" id="t" meta:validator="filled"><br>
  Select:
  <select name="sel">
    <option value="a">aaa</option>
    <option value="b">bbb</option>
  </select><br>
  <input type="submit" name="doSend" value="Send!">
</form>
<pre>
Errors: <?print_r($metaFormAction->getErrors())?>
</pre>

Метод $metaFormAction->process() работает следующим образом:

  1. В случае, если ни одна кнопка не была нажата, либо же скрипт запущен просто набором его URL в браузере, возвращает фиксированную строку 'INIT' и больше ничего не делает. В этот момент можно загрузить данные, которые должны быть выведены в форме в качестве значений по умолчанию.
  2. Запускает проверки цифровой подписи, а также допустимости полученных данных (контроль URL обработчика, неизменности hidden-полей, существования выбранных элементов в списках и т. д.).
  3. Запускает ассоциированные с элементами валидаторы, указанные в специальных мета-атрибутах meta:validator.
  4. Если ошибок не обнаружено, возвращает имя нажатой submit-кнопки. Если же ошибки были, возвращает null и позволяет получить список ошибок через вызов $metaFormAction->getErrors().

Валидаторы

Каждому полю формы можно присвоить специальный атрибут meta:validator, указывающий, какие именно проверки значений нужно запустить. Формат значения атрибута следующий (квадратными скобками обозначены необязательные элементы):

Листинг 5
meta:validator="[@][!]валидатор1[:модиф1] [@][!]валидатор2[:модиф2] ..."

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

  1. проверяемое значение, введенное пользователем;
  2. элемент метаданных, для которого выполняется проверка.

В качестве функции-валидатора validator_валидаторN() может также выступать и одноименный метод класса HTML_MetaFormAction (или производного от него, если вы используете наследование). Именно так реализован валидатор с именем filled, уже имеющийся в HTML_MetaFormAction:

Листинг 6
/**
 * Validator: check for field fillness.
 *  
 * @param string $value  Input value to check.
 * @return               True if value is empty (not filled).
 */
function validator_filled($value)
{
    return is_scalar($value)? !!trim($value) : !empty($value);
}

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

  • Определить обычную функцию PHP с именем validator_имя() и одним параметром (или двумя, если информация о текущем элементе формы нужна в валидаторе).
  • Создать класс, производный от HTML_MetaFormAction, и определить в нем методы-валидаторы (они будут в общем случае специфичны для каждого конкретного проекта). Далее везде вместо HTML_MetaFormAction использовать этот новый класс.

Чайник 

Библиотека требует, чтобы имя функции-валидатора всегда начиналось с префикса validator_, по соображениям безопасности. Даже если злоумышленнику удастся внедрить свой собственный HTML-код в текст страницы, он все равно не сможет запустить с его помощью произвольную функцию PHP, как это было бы, не используй мы префикса.

Значение, возвращаемое валидатором, трактуется следующим образом.

  • Если значение — скаляр (число, строка, булевское значение и т. д.), то значение, равное false, означает ошибку валидации, все остальное — ее успех. При ошибке валидации в список, возвращаемый по getErrors(), добавляется элемент, в котором указывается: имя валидатора, имя поля формы и его атрибут id.
  • Если значение — массив (или объект), то он всегда трактуется как ошибка валидации. При этом в список, возвращаемый по getErrors(), добавляется вся та же информация, что была перечислена выше, но — дополнительно проставляется атрибут message, содержащий возвращенное валидатором значение. Этот атрибут можно использовать при выводе детальной диагностики в форме.

Модификаторы и префиксы валидаторов

Рассмотрим еще раз формат строки определения валидаторов для поля:

Листинг 7
meta:validator="[@][!]валидатор1[:модиф1] [@][!]валидатор2[:модиф2] ..."

Каждый валидатор в списке может иметь свой собственный модификатор модифN, уточняющий его действие. В настоящее время поддерживается только один модификатор — manual, запрещающий автоматическое выполнение валидатора по вызову process() без параметров. Чтобы запустить manual-валидаторы, необходимо вручную вызвать в обработчика $metaFormAction->process(array('поле1', 'поле2', ...)), где полеN — имя очередного проверяемого поля; в таком случае вызываются только manual-валидаторы и только для указанных полей.

Кроме того, имя валидатора может начинаться со знака @, что подавляет выдачу сообщений E_NOTICE при выполнении валидатора. Также допустимо инвертировать действие валидатора, используя префикс ! (например, если filled означает "поле заполнено", то !filled — "поле пусто").

Ручной запуск валидаторов

Часто встречаются формы, в которых те или иные поля должны проверяться по-разному в зависимости от значений, выбранных в других полях. Например, если в некоторой форме установлен переключатель "создать документ", то поле "текст документа" должно быть обязательно заполненно (валидатор filled), а если установлен переключатель "создать категорию", то оно не обязательно (и игнорируется). Логику проверки может "знать" только скрипт-обработчик формы, в шаблоне ее задать весьма сложно.

Чтобы учитывать подобные ситуации, необходимо приписать полю "текст документа" валидатор с модификатором manual:

Листинг 8
<textarea name="txt" meta:validator="filled:manual">

Далее в нужном месте обработчика следует вызвать следующий код:

Листинг 9
switch ($metaFormAction->process()) {
    ...        
    case 'doSend':
        // Process field 'txt' manually.
        if (!$metaFormAction->process(array('txt'))) break;
        // Process entered data here.
        ...
        break;
    ...
}

Таким образом, поле txt будет исключено из первоначальной проверки (срабатывает модификаторс manual), но в дальнейшем оно проверяется отдельно. В случае ошибки — работа обработчика формы прекращается.

Поля, изменяемые на JavaScript

Библиотека HTML_MetaFormAction проверяет, чтобы значение, полученное из hidden-поля, совпадало с тем, что находилось ранее в его атрибуте value. Если это не так, генерируется ошибка валидации metaformaction_invalid_value: считается, что данные подделал злоумышленник, подменивший хранимую в hidden-поле величину. Такая же техника применяется по отношению к атрибуту action формы (он сверяется с REQUEST_URI), а также к элементам выпадающих список, радиокнопок и checkbox-ов (невозможно выбрать несуществующий элемент).

Однако как быть, если то или иное поле формы заполняется скриптом на JavaScript? Например, он может записать в hidden-поле time время, потраченное пользователем на заполнение формы перед ее отправкой. Естественно, HTML_MetaFormAction сгенерирует ошибку: недопустимое значение в hidden-поле.

Чтобы расширить диапазон значений, принимаемых hidden-полем (или другими видами полей, перечисленными выше), существует мета-атрибут meta:dynamic:

Листинг 10
<input type="hidden" name="time" id="t" value="0" meta:dynamic>
<input type="hidden" name="fast" id="f" value="false" meta:dynamic="true">
<input type="hidden" name="list" id="l" value="a" meta:dynamic="b c d">
<script>
  document.getElementById('t').value = 101;
  document.getElementById('f').value = 'true';
  document.getElementById('l').value = 'b';
</script>

Атрибут meta:dynamic, указанный без значения, разрешает использовать любое значение поля. Если же указать meta:dynamic="val", то к списку допустимых значений (в нашем примере это "0") добавляется строка, указанная в качестве значения атрибута ("val"). Наконец, можно указать сразу несколько разрешенных значений в одном атрибуте meta:dynamic, отделив их друг от друга ровно одним пробелом.

Ошибки валидации

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

  • Ключ name: имя элемента формы, породившего ошибку валидации (значение атрибута name тэга).
  • Ключ message: текст сообщения об ошибке, если он был сгенерирован валидатором. Если валидатор просто вернул ложное значение, в данном поле окажется null, и сообщение пользователю надо строить на основе других полей. Значение, соответствующее данному ключу, не обязательно является строкой: чаще всего это как раз не так. Например, если значение, полученное для hidden-поля, недопустимо, в message будет содержаться array('metaformaction_invalid_value', $name, $type), где $name — имя поля, а $type — его тип (обычно text).
  • Ключ validator: полное имя сработавшей функции-валидатора (если это метод класса, то имя класса указывается перед ::, в нижнем регистре).
  • Ключ meta: метаданные поля, породившего ошибку валидации.

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

Листинг 11
<?php ## t_errors.php: display validation errors.
include_once "../../lib/config.php";
include_once "../../HTML_FormPersister/lib/config.php";
include_once "HTML/MetaForm.php"; 
include_once "HTML/MetaFormAction.php"; 
include_once "HTML/FormPersister.php"; 

// Assign output processor to pass metadata.
$metaForm =& new HTML_MetaForm('secret_digital_signature_YS0lTgit');
ob_start(array(&$metaForm, 'process'));

// Turn on FormPersister for all HTML forms.
ob_start(array('HTML_FormPersister', 'ob_formpersisterhandler'));

// Now process the form (if posted).
$metaFormAction =& new HTML_MetaFormAction($metaForm);
switch ($metaFormAction->process()) {
    case 'INIT':
        // Called when script is called via GET method.
        // No buttons are pressed yet, initialize form fields.
        $_POST['age'] = rand(10, 20);
        break;
        
    case 'doSend':
        // Called when doSend is pressed and THERE ARE 
        // NO VALIDATION ERRORS! Process the form.
        break;
}

// Return true if value is natural number.
function validator_natural($value)
{
    return is_numeric($value) && $value >= 1;
}
?>

<!-- Show error texts. -->
<div style="background:#FFBBBB; margin-bottom:1em">
  <?foreach ($metaFormAction->getErrors() as $e) {?>
    <?if ($m = $e['message']) {?>
      <?=join(", ", $m)?>
    <?} else {?>
      "<?=preg_replace('/.*::/', '', $e['validator'])?>"
      failed for field "<?=$e['meta']['label']?>"!
    <?}?>
    <br>
  <?}?>
</div>

<!-- Display the form. -->
<form method="POST">
  <label for="t1">First name</label>:
  <input type="text" name="first" id="t1" meta:validator="filled"><br>
  <label for="t2">Age</label>:
  <input type="text" name="age" id="t2" meta:validator="natural"><br>
  <input type="submit" name="doSend" value="Send!">
</form>

<!-- Highlight error fields. -->
<script>
<?foreach ($metaFormAction->getErrors() as $e) {?>
  var e = document.getElementById('<?=@$e['meta']['id']?>')
    || document.getElementsByName('<?=@$e['name']?>')[0];
  e.style.border = '2px solid red';
<?}?>
</script>

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

Чайник 

Конечно, в реальных приложениях надо выводить не имя функции-валидатора, а его текстовое название. Это же касается и текста ошибки, указанного в message: если там строка вида metaformaction_invalid_value, нужно преобразовать ее в локализованный текст перед выводом. Получить читаемый человеком текст по строковой константе можно, например, табличным способом: завести ассоциативный массив, в ключах которого хранить константы, а в значениях — сообщения на русском (или английском) языке (здесь еще очень удобно использовать функцию sprintf для подстановки значений параметров ошибки вместо маркеров %s). В общем, достаточно традиционный способ локализации приложений.

Автозаполнение полей формы

Чайник 

Данный раздел является кратким введением в библиотеку HTML_FormPersister, описанную в статье HTML_FormPersister: новый взгляд на построение форм.

Обратите внимание на страный фрагмент предыдущего листинга:

Листинг 12
$_POST['age'] = rand(10, 20);

Вы заметите, что благодаря ему при первой загрузке формы в поле "Age" магическим образом появляется случайное число в диапазоне 10..20, хотя соответствующий тэг <input type="text" name="age"> вообще не содержит атрибута value.

Это и есть результат работы библиотеки HTML_FormPersister. Благодаря ей значения, имеющиеся в массиве $_POST (а также $_GET) автоматически попадают в одноименные поля формы, у которых не проставлен атрибут value. Это касается абсолютно любых элементов формы: текстовых и скрытых полей, выпадающих списков, флажков, радиокнопок, textarea.

Почему в качестве источника данных выбран именно $_POST, а не какая-то другая переменная? Да потому, что это позволяет сохранять введенные ранее значения, если форма была отображена путем отправки POST-запроса на сервер. Например, на рисунке выше мы ввели "19a" в поле "Age" и нажали кнопку отправки. Это значение попало в $_POST['age'], произошла ошибка валидации ("19a" — не число), и тут же была отображена та же самая форма. Благодаря HTML_FormPersister ранее введенные данные никуда не пропали, а остались на месте. Вообще, действует правило: если что-то введено в произвольную форму, она отправлена на сервер и отображена снова, то все введенные данные гарантировано остаются на своих местах.

Полный список возможностей, предоставляемых HTML_FormPersister, вы можете прочитать в статье HTML_FormPersister: новый взгляд на построение форм.

Резюме

Написание скриптов, работающих с формами, традиционно относят к наиболее рутинной, трудоемкой и подверженной проблемами в безопасности операцией. Ситуация усложняется тем, что информация о структуре формы (метаданные) оказываются "размазанными" между HTML-шаблоном формы и скриптом-обработчиком, в результате чего сильно усложняется модификация уже существующих форм (приходится править код сразу в нескольких местах). Благодаря решению, заложенному в библиотеке HTML_MetaForm и сопутствующих ей модулях HTML_MetaFormAction и HTML_FormPersister, все эти проблемы могут уйти в прошлое, а написание Web-скриптов, обрабатывающих формы, становится таким же простым, как создание пользовательского интерфейса в GUI-приложениях.





Высшая школа - бизнес образование дистанционно для руководителей | Самая подробная информация поступить на дистанционное обучение на сайте.


Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com