Свинцовый пепелац

Асинхронная загрузка файлов через скрытый IFRAME: IE отличился

25 октября 2013

Описанный в этой заметке способ асинхронной загрузки файлов основан на предусмотренной в HTML-стандарте возможности передачи данных из формы в любой IFRAME, имеющийся на странице. Форма связана с IFRAME через атрибут «target», который должен совпадать с атрибутом «name» у IFRAME.

На данную тему в интернете можно найти много заметок, но описание подводных камней такого случая, в котором при помощи JavaScript создаётся не только скрытый IFRAME, но и вся форма, мне не попалось, поэтому решил поделиться своим опытом в этом вопросе.

Начал с того, что написал своего рода HTML прототип, чтобы установить адекватность работы этого метода во всех браузерах.

<html>
  <head></head>
  <body>
    <form action="upload.php" method="post" enctype="multipart/form-data" target="frame">
      <input type="file" name="upload" onchange="submit();" />
    </form>
    <iframe src="upload.php" name="frame"></iframe>
  </body>
</html>

Чтобы отслеживать, что приходит с формы на сервер, создал файлик «upload.php» со следующим кодом:

<html>
  <head></head>
  <body>
    <?php print('$_POST:' . print_r($_POST, 1) . ' $_FILES:' . print_r($_FILES, 1)); ?>
  </body>
</html>

Проверил - работает даже в IE (Internet Explorer 8 в режиме IE7).

А теперь попробую сделать то же самое при помощи JavaScript. При изучении интернет публикаций по данной теме неоднократно натыкался на замечания разных авторов, что со скрытым IFRAME могут возникать всякие проблемы в разных браузерах, поэтому скрывать его лучше оборачиванием в скрытый DIV (display:none). В приведённых примерах скрываю IFRAME именно таким образом.

Документ:

<html>
  <head>
    <script type="text/javascript" src="script.js"></script>
  </head>
  <body></body>
</html>

JavaScript часть - файл «script.js»:

  var frame, form;
  window.onload = function() {
    // Создаём форму и устанавливаем значения атрибутов
    form = document.createElement("FORM");
    form.setAttribute('action', 'upload.php');
    form.setAttribute('method', 'post');
    form.setAttribute('enctype', 'multipart/form-data');
    form.setAttribute('target', 'frame');
    // Создаём INPUT и устанавливаем значения атрибутов
    var input = document.createElement("INPUT");
    input.setAttribute('type', 'file');
    input.setAttribute('name', 'upload');
    // Навешиваем на INPUT обработчик события onChange
    if (input.addEventListener)
      input.addEventListener("change", function() {upload()}, false);
    else
      input.attachEvent("onchange", function() {upload()});
    // Помещаем INPUT в форму
    form.appendChild(input);
    // Помещаем форму на страницу 
    document.body.appendChild(form);
    // Создаём IFRAME и устанавливаем значения атрибутов
    frame = document.createElement("IFRAME");
    frame.setAttribute('src', 'upload.php');
    frame.setAttribute('name', 'frame');
    // Создаём DIV обёртку для IFRAME
    var wrapper = document.createElement('DIV');
    // Помещаем IFRAME в DIV обёртку
    wrapper.appendChild(frame);
    // Делаем обёртку (и её содержимое) невидимой
    wrapper.style.display = 'none';
    // Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
    document.body.appendChild(wrapper);
  }

плюс функция, которая назначена в обработчике события «onChange» элемента INPUT:

  function upload() {
    // Сабмитим форму
    form.submit();
    // Выводим результат alert-ом
    setTimeout(function() {
      alert(frame.contentWindow.document.body.innerHTML);
    }, 1000)
  }

Я думал, что сделал всё по уму, но не тут-то было. В Mozilla Firefox, Google Chrome и Opera всё работает именно так, как и должно было, но вот IE отличился. Все элементы создались, на первый взгляд, нормально, но при сабмите формы, почему-то, данные отправлялись не в IFRAME, а в новое окно, при этом массив $_FILES оказался пустым, а в массиве $_POST появился элемент с ключом «upload». Очевидно, что данные с моего INPUT почему-то попали не в $_FILES, а в $_POST. Такая ситуация возникает, если у формы не указан способ кодирования данных при их отправке на сервер "multipart/form-data" (атрибут «enctype»). Удивился. Стал смотреть DOM средствами разработчика.

Изучение DOM-а показало что атрибут «enctype» у формы стоит именно такой, как и нужно, а вот INPUT и IFRAME вместо атрибута «name» имеют атрибут «submitName». Удивился ещё сильнее. Стало понятно, почему данные с формы улетели не в IFRAME, а в новое окно - потому что IFRAME с именем указанным в атрибуте «target» у формы на странице отсутствует (это имя, почему-то, превратилось в атрибут «submitName»).

Хотя ситуация с попаданием данных с INPUT-а в массив $_POST вместо $_FILES пока и осталась непонятной, решил начать с устранения проблемы атрибутов «name» у INPUT и IFRAME.

Попробовал задавать значение атрибута «name» не через метод setAttribute(), а просто как свойство объекта: input.name = 'upload' и frame.name = 'frame'. Результат тот же.

Погуглив, быстро выяснил, что проблема «submitName» у IE действительно имеет место быть. Решение этой проблемы оказалось очень простым - нужно создавать элементы через innerHTML сразу с прописанным атрибутом «name». Понравилось решение, найденное на просторах интернета, позволяющее получить элемент в виде объекта, не смотря на то, что он, по сути, создаётся при помощи innerHTML. Для удобства использования оформил этот способ в функцию.

  function createElm(A) {
    if (typeof A == 'string') {
      var B = document.createElement('div');
      B.innerHTML = A;
      return B.firstChild;
    }      
  }

Следует обратить внимание на то, что при использовании этого подхода в IE почему-то не удаётся задать для INPUT атрибут «type» через setAttribute() или через свойство объекта, поэтому type="file" пришлось тоже прописывать в innerHTML.

  var frame, form;
  window.onload = function() {
    // Создаём форму и устанавливаем значения атрибутов
    form = document.createElement("FORM");
    form.setAttribute('action', 'upload.php');
    form.setAttribute('method', 'post');
    form.setAttribute('enctype', 'multipart/form-data');
    form.setAttribute('target', 'frame');
    // Создаём INPUT (сразу с атрибутами type и name)
    var input = createElm('<input type="file" name="upload" />');
    // Навешиваем на INPUT обработчик события onChange
    if (input.addEventListener)
      input.addEventListener("change", function() {upload()}, false);
    else
      input.attachEvent("onchange", function() {upload()});
    // Помещаем INPUT в форму
    form.appendChild(input);
    // Помещаем форму на страницу 
    document.body.appendChild(form);
    // Создаём IFRAME (сразу с атрибутом name) и устанавливаем значения атрибутов
    frame = createElm('<iframe name="frame"></iframe>');
    frame.setAttribute('src', 'upload.php');
    // Создаём DIV обёртку для IFRAME
    var wrapper = document.createElement('DIV');
    // Помещаем IFRAME в DIV обёртку
    wrapper.appendChild(frame);
    // Делаем обёртку (и её содержимое) невидимой
    wrapper.style.display = 'none';
    // Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
    document.body.appendChild(wrapper);
  }

Наконец-то заработало и в IE.

Но проблема в IE с тем, что данные с INPUT-а формы не попадают в массив $_FILES, всё ещё сохранилась. Ещё раз внимательно посмотрел DOM средствами разработчика - у формы действительно стоит enctype="multipart/form-data". WTF???!!! После долгих раздумий, экспериментов и гуглений, попробовал прописать у формы атрибут «enctype» прямо в innerHTML.

  var frame, form;
  window.onload = function() {
    // Создаём форму (сразу с атрибутом enctype) и устанавливаем значения атрибутов
    form = createElm('<form enctype="multipart/form-data"></form>');
    form.setAttribute('action', 'upload.php');
    form.setAttribute('method', 'post');
    form.setAttribute('target', 'frame');
    // Создаём INPUT (сразу с атрибутами type и name)
    var input = createElm('<input type="file" name="upload" />');
    // Навешиваем на INPUT обработчик события onChange
    if (input.addEventListener)
      input.addEventListener("change", function() {upload()}, false);
    else
      input.attachEvent("onchange", function() {upload()});
    // Помещаем INPUT в форму
    form.appendChild(input);
    // Помещаем форму на страницу 
    document.body.appendChild(form);
    // Создаём IFRAME (сразу с атрибутом name) и устанавливаем значения атрибутов
    frame = createElm('<iframe name="frame"></iframe>');
    frame.setAttribute('src', 'upload.php');
    // Создаём DIV обёртку для IFRAME
    var wrapper = document.createElement('DIV');
    // Помещаем IFRAME в DIV обёртку
    wrapper.appendChild(frame);
    // Делаем обёртку (и её содержимое) невидимой
    wrapper.style.display = 'none';
    // Помещаем невидимую DIV обёртку содержащую IFRAME на страницу
    document.body.appendChild(wrapper);
  }

О, чудо!!! Заработало!!! В свете данного открытия встаёт закономерный вопрос - на сколько можно доверять средствам разработчика IE?

Ну и в заключении, чтобы загрузка файлов таки реально заработала, поменял код файла «upload.php»:

<html>
  <head></head>
  <body>
<?php
  $error = '';
  $uploadErr = array(
    1 => 'Размер принятого файла изображения превысил максимально допустимый размер, который задан директивой upload_max_filesize конфигурационного файла php.ini',
    2 => 'Размер загружаемого файла изображения превысил значение MAX_FILE_SIZE, указанное в HTML-форме',
    3 => 'Загружаемый файл изображения был получен только частично',
    4 => 'Файл изображения не был загружен',
    5 => 'Отсутствует временная папка',
    6 => 'Не удалось записать файл изображения на диск'
  );
  if (isset($_FILES['upload']) && @$_FILES['upload']['error'] && @$_FILES['upload']['error'] != 4)
    $error = $uploadErr[$_FILES['upload']['error']];
  if (isset($_FILES['upload']) && !$error && !@$_FILES['upload']['error']) {
    if (!@move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $_FILES['upload']['name']))
      $error = 'Не удалось переместить загруженный файл';
  }
  if (isset($_FILES['upload']) && !$error)
    print('Файл `' . htmlspecialchars($_FILES['upload']['name']) . '` успешно загружен');
  else
    print($error);
?>
  </body>
</html>

Файлы загружаются в папку «uploads», алерты сообщают о результатах загрузки. Работает во всех браузерах, в том числе и в IE. Цель достигнута.

В прикреплённом к статье архиве Вы найдёте окончательный вариант скрипта асинхронной загрузки файлов через скрытый IFRAME.

Метки: JavaScript, PHP

Прикреплённые файлы

Написать комментарий

Имя:
e-mail
(НЕ обязателен, НЕ публикуется на сайте):
Сообщение:
Прикрепить изображение (не более 5 Мб):