Обработка HTML-форм, содержащих бинарные данные, на JavaScript/ASP

Автор: Анатолий Садовский
Источник: RSDN Magazine #2-2003
Опубликовано: 12.07.2003
Версия текста: 1.0
Предисловие
Получение данных от клиента
Разделение данных на поля
Преобразование в текст
Разделение на поля
Обработка поля с содержимым файла
Заключение
Cсылки

Предисловие

Некоторое время назад передо мной возникла задача передачи файлов на сервер по HTTP-протоколу и их загрузки в базу данных. Чаще всего это решается использованием специальных ActiveX-компонентов, но это не всегда безопасно и не всегда это решение является рациональным. Поэтому я поставил перед собой цель реализовать это на ASP без применения дополнительных компонентов. До сих пор мне не приходилось работать с HTML-формами, содержащими файлы или двоичные данные, поэтому я начал с того, что открыл Internet Explorer и попытался найти нужную информацию в мировой паутине. Однако передо мной возникла ещё одна проблема. Дело в том, что большая часть кода системы уже была написана на JavaScript, а все статьи, которые я находил, содержали примеры на VBScript. Так как мне очень не хотелось смешивать два языка в одной системе, я решил транслировать найденные примеры в JavaScript. Естественно, я столкнулся при этом с некоторыми трудностями, возникшими при написании кода для разбора данных, пришедших от пользователя. В этой статье я хочу поделиться своими решениями с читателями журнала.

Получение данных от клиента

Рассмотрим, как сервер получает данные от клиента. Сообщение типа multipart/form-data состоит из нескольких частей. Части отделяются друг от друга разделителями полей (boundary). Разделитель имеет переменное значение и может меняться в зависимости от нескольких факторов. Кроме того, разделитель может быть окружен произвольным числом символов “-“. Каждая часть сообщения содержит заголовок Content-Disposition, имеющий значение form-data, атрибут наименования, определяющий имя соответствующего управляющего элемента HTML-формы, и, собственно данные, составляющие управляющий элемент. Для разделения строк данных используется комбинация CR+LF (т.е. последовательность кодов символов 13+10). Если клиент передает некоторый файл, то имя файла указывается в параметре filename заголовка Content-Disposition: form-data.

Получить всю эту информацию можно при помощи встроенного объекта Request. Однако не пытайтесь получить значения полей формы, используя привычную конструкцию Request.Form("имя_поля")(), где имя_поля – имя элемента HTML-формы. Если форма имеет тип содержимого multipart/form-data, то это выражение вернет значение undefined. Происходит это потому, что данные от клиента в этом случае поступают в бинарном виде, и должны быть прочитаны с помощью метода BinaryRead объекта Request. В качестве параметра для этого метода нужно указать количество считываемых байтов. Общее количество данных можно узнать с помощью свойства TotalBytes объекта Request.

Рассмотрим простой пример. Создадим HTML-форму, не забыв при этом указать для атрибута enctype значение multipart/form-data:

<form name="test" method="post" enctype="multipart/form-data" action="test.asp">
  Поле №1: <input type="text" name="Field1"><br>
  Поле №2: <input type="text" name="Field2"><br>
  Файл №1: <input type="file" name="FileData1"><p>
  <input type="submit" value="Тест">
</form>

После этого создадим файл test.asp. Напишем в нем код для определения разделителя полей. Для этого можно воспользоваться переменной среды IIS CONTENT_TYPE, в которой и содержится тип данных, включённых в запрос, а также искомый разделитель. Для наглядности выведем значения переменных и разделителя в браузер:

Листинг 1.
<%
  var Header = new String(Request.ServerVariables("CONTENT_TYPE"));
  var Boundary = Header.slice(Header.length 
    - Header.indexOf("boundary=") - 8);
  Boundary = Boundary.replace(/-/g,"");
  Response.Write("Заголовок: " + Header);
  Response.Write("<hr>Разделитель: " + Boundary);
%>

Разделение данных на поля

Преобразование в текст

Теперь необходимо получить тело запроса и выделить из него отдельные поля, соответствующие элементам HTML-формы. Необходимо учесть, что разделитель был получен нами в текстовом формате, а тело запроса в бинарном, поэтому если мы попытаемся искать наш «текстовый» разделитель в «бинарном» теле запроса, то, скорее всего, ничего не найдем. Как же быть? Так как JavaScript не умеет работать с бинарными данными, то выход один – преобразовать тело запроса в текстовый формат и искать разделитель в преобразованном запросе. Сделать такое преобразование можно с помощью объекта ADO Recordset:

Листинг 2.
<%
  // Преобразование бинарных данных в текстfunction BinToText(DataBin) 
  {
    var objRS = Server.CreateObject("ADODB.Recordset");
    objRS.Fields.Append("txt",201,Request.TotalBytes);
    objRS.Open();
    objRS.AddNew;
    objRS.Fields("txt").AppendChunk(DataBin);
    objRS.Update;
    var DataTextRes = objRS("txt").Value;
    objRS.Close();
    objRS = null;
    return DataTextRes;
  }
%>

Функция BinToText создает объект Recordset и определяет в нем поле данных типа LongVarChar, которому присваивает имя txt. В это поле затем записываются двоичные данные, переданные функции, которые сразу же читаются из него уже в текстовом виде. После добавления функции BinToText в файл test.asp можно будет вывести результат преобразования в браузер и посмотреть что получилось. Для этого к тексту, приведённому в Листинге 1, необходимо дописать следующий код:

Листинг 3.
<%
  var DataText = BinToText(Request.BinaryRead(Request.TotalBytes));
  Response.Write("<hr>Тело запроса в текстовом виде: " + DataText);
%>

Для примера введем в Поле №1 «Интернет», а в Поле №2 «Коммуникации». Вот что мы увидим в окне браузера:


Разделение на поля

Теперь разделим полученное в текстовом виде тело запроса на отдельные поля. Для начала определимся, какая информация о полях нас интересует. Вероятно, это имя поля, данные, тип содержимого и размер данных. Для хранения этой информации определим объект FormField:

Листинг 4.
<%
  // Объект "Поле формы"function FormField() 
  {
    this.Name = null;
    this.Value = null;
    this.ContentType = null;
    this.Size = 0;
  }
%>

Очевидно, что тип содержимого и размер важны только для поля, содержащего бинарные данные, в нашем примере это файл. Для разбиения по полям я написал отдельную функцию. Эта функция возвращает массив объектов FormField, содержащий информацию обо всех полях, полученных от клиента:

Листинг 5.
<%
  // Получение информации по всем полям запросаfunction GetFormFields() 
  {
    // Получаем заголовокvar Header = new String(Request.ServerVariables("CONTENT_TYPE"));
    // Получаем разделительvar Boundary = Header.slice(Header.length 
      - Header.indexOf("boundary=") - 8);
    Boundary = Boundary.replace(/-/g,"");
    // Получаем тело запроса в текстовом видеvar DataText = BinToText(Request.BinaryRead(Request.TotalBytes));
    // Убираем первый разделитель
    DataText = DataText.slice(DataText.indexOf(Boundary) 
      + Boundary.length);  
    // Убираем последний разделитель
    DataText = DataText.slice(0,DataText.lastIndexOf(Boundary));
    // Разделяем на поля - получаем массив строк DataPartvar DataPart = DataText.split(Boundary);
    // Убираем ненужные символы в начале и в конце каждого поляfor (var i=0;i<DataPart.length;i++) 
      DataPart[i] = DataTrim(DataPart[i]);
    var FieldStr = "";
    var FF = new Array();
    // Разбираем поля по частямfor (var i=0;i<DataPart.length;i++)
    {
      FF[i] = new FormField();
      FieldStr = DataPart[i];
      pn = FieldStr.indexOf("name=\"") + 6;
      // Имя поля
      FF[i].Name = FieldStr.slice(pn,FieldStr.indexOf("\"",pn));
      // Если это файл, то поле содержит бинарные данные
      pfn = FieldStr.indexOf("filename=\"");
      if (pfn >= 0)
      {
        pfn = pfn + 10;
        // Тип содержимого
        pct = FieldStr.indexOf("Content-Type: ",pfn) + 14;
        pct_end = FieldStr.indexOf("\n",pct);
        if (pct_end < 0) pct_end = FieldStr.length;
        FF[i].ContentType = FieldStr.slice(pct,pct_end);
        // Данные бинарные
        DataFile = FieldStr.slice(pct_end + 3);
        FF[i].Value = TextToBin(DataFile);
        // Размер данных
        FF[i].Size = DataFile.length;
      }
      else
      {
        // Данные текстовые
        FF[i].Value = FieldStr.slice(FieldStr.indexOf("\"",pn) + 5);
      }
    }
    return FF;
  }
%>

При этом мы использовали вспомогательную функцию DataTrim, убирающую ненужные символы в начале и в конце строки. Текст её приведён ниже:

Листинг 6.
<%
  function DataTrim(Str)
  {
    while ((Str.charCodeAt(1) == 13) || (Str.charCodeAt(1) == 10)
            || (Str.charAt(1) == "-"))
      Str = Str.slice(1);
    while ((Str.charCodeAt(Str.length - 1) == 13) 
            || (Str.charCodeAt(Str.length - 1) == 10)
            || (Str.charAt(Str.length - 1) == "-"))
      Str = Str.slice(0,Str.length - 1);
    return Str;
  }	
%>

Обработка поля с содержимым файла

Процесс обработки поля с содержимым файла заслуживает особого разговора. В отличие от других полей, здесь дополнительно определяются тип содержимого и размер данных, включённых в поле. Кроме этого, данные такого поля конвертируются обратно в бинарный формат для дальнейшего использования. С написанием именно этого преобразования у меня и возникли самые большие сложности, потому что, как я уже говорил, JavaScript не умеет работать с бинарными данными. Решение было найдено в использовании объекта ADO Stream для работы с потоками:

Листинг 7.
<%
  // Преобразование текстовых данных в бинарныеfunction TextToBin(DataText) 
  {
    var TextStream = Server.CreateObject("ADODB.Stream");
    var BinStream = Server.CreateObject("ADODB.Stream");
    TextStream.Type = 2;
    TextStream.Charset = "Windows-1251";
    BinStream.Type = 1;
    TextStream.Open;
    TextStream.WriteText(DataText);
    BinStream.Open;
    TextStream.Position = 0;
    TextStream.CopyTo(BinStream,-1);
    BinStream.Position = 0;
    var DataBinRes = BinStream.Read;
    TextStream.Close;
    BinStream.Close;
    TextStream = null;
    BinStream = null;
    return DataBinRes;
  }
%>

Работает функция TextToBin очень просто. Создаются два потока, текстовый и бинарный. В текстовый поток записываются полученные функцией данные, после чего текстовый поток копируется в бинарный. Данные читаются из бинарного потока, после прочтения поток удаляется и функция возвращает результат чтения уже в бинарном виде.

Добавим все эти функции в test.asp и напишем код, который опробует их и выведет информацию о полях запроса в браузер.

Листинг 8.
<% 
  // Объект "Поле формы"function FormField() 
  {
    ...
  }
  // Преобразование бинарных данных в текстfunction BinToText(DataBin) 
  {
    ...
  }
  // Преобразование текстовых данных в бинарныеfunction TextToBin(DataText) 
  {
    ...
  }
  // Функция, убирающая ненужные символы в начале и в конце строкиfunction DataTrim(Str) 
  {
    ...
  }
  // Получение информации по всем полям запросаfunction GetFormFields() 
  {
    ...
  }

  // Получим поля запросаvar FormFields = GetFormFields();
  // Выведем информацию о полях в браузерfor (var i=0;i<FormFields.length;i++) 
  {
    Response.Write("<hr>Имя поля: " + FormFields[i].Name);
    Response.Write("<br>Значение: " + FormFields[i].Value);
    Response.Write("<br>Тип содержимого: " + FormFields[i].ContentType);
    Response.Write("<br>Размер: " + FormFields[i].Size);
  }
%>

Итак, мы получили все поля с данными, поступившими от клиента, при этом использовали «чистый» JavaScript без применения каких-либо нестандартных компонентов, и тем самым достигли поставленной цели. Для удобства использования можно написать еще одну функцию, которая, аналогично Request.Form("имя_поля")(), позволяет обращаться к полям запроса по имени:

Листинг 9.
<%
  // Находит поле формы по имениfunction GetFormFieldByName(FF,n) 
  {
    var i = 0;
    var found = false;
    var Res = null;
    while ((i < FF.length) && (!found)) 
    {
      if (FF[i].Name == n) 
      {
        Res = FF[i];
        found = true;
      }
      i++;
    }
    if (Res != null) return Res;
    returnnew FormField();
  }
%>

Функция возвращает объект FormField для поля с именем n из массива объектов полей FF. Например, следующий код получает данные поля с именем Field1:

Листинг 10.
<%
  var FormFields = GetFormFields();
  var Data_Field1 = GetFormFieldByName(FormFields,"Field1").Value;
%>

Заключение

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

Cсылки

  1. HTML 4.0 Specification. Form content types. http://www.w3.org/TR/REC-html40-971218/interact/forms.html#h-17.13.4
  2. Андрей Столяров. Загрузка файлов на сервер с использованием HTTP-протокола. http://koi.activeserverpages.ru/articles/read.asp?id=24
  3. ADO 2.8 API Reference. Recordset Object. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ado270/htm/mdaobj01_19.asp
  4. ADO 2.8 API Reference. Stream Object. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ado270/htm/mdaobj01_21.asp
  5. How can I convert binary data into a string? http://www.4guysfromrolla.com/aspfaqs/ShowFAQ.asp?FAQID=128
  6. Using the ADO Stream Object to Manage BLOBs. http://betav.com/files/content/whitepapers/using%20the%20ado%20stream%20object%20to%20manage%20blobs.htm


Эта статья опубликована в журнале RSDN Magazine #2-2003. Информацию о журнале можно найти здесь