Недавно я делал форму обратной связи с возможностью загрузки файлов. При этом стояла задача сделать форму идеально безопасной, с "белым списком" файлов, а также на выходе формировать письмо клиентскому отделу с данными клиента и всеми его файлами. Поскольку файлы (согласно ТЗ) могут иметь большой размер (до 50 МБ каждый), никакой почтовый клиент не выдержит такого объёма информации, особенно учитывая то, что размеры почтовых ящиков у менеджеров всегда переполнены и при этом им никак нельзя пропустить заявку от клиента.
Я принял решение формировать url для ftp-соединения в браузере, с включённым паролем. Для этой цели я создал на VPS отдельного пользователя и ограничил ему доступ только одной папкой, в которой будут документы. Таким образом, сторонние пользователи никак не смогут просмотреть данные файлы, а менеджеры клиентского отдела имеют только на просмотр, причём исключительно в рамках одной папки.
Все основные настройки я вынес в начало скрипта.
Оригинал скрипта Вы можете скачать через bitbucket: https://bitbucket.org/lisogorsky/dev/src/master/uploadsFilesForm/
Итак, как создать безопасную форму на php для загрузки файлов на сервер
Первым делом настроим php.ini по адресу /etc/php.ini
Если мы не сделаем это, то размер принимаемых файлов может быть сильно ограничен. Например, на нашем VPS стояло ограничение в 2 Мб.
post_max_size ставим 50М
upload_max_filesize ставим меньше, 49М
параметр memory_limit должен быть больше, чем post_max_size
Для поля ввода с телефоном можем подключить Jquery maskedinput
Создаём папку в родительском каталоге, например 'clients_documents' (её название потом пропишем в скрипте ниже)
<? header('Content-Type: text/html; charset=utf-8'); /* *** ОСНОВНЫЕ НАСТРОЙКИ *** */ // Сюда вписываем все разрешённые для загрузки расширения файлов: $goodFiles = ['.doc','.docx','.pdf','.xls','.xlsx','.ppt','.pptx','.zip','.rar','.7z','.jpg','.jpeg','.jpe','.bmp','.png','.txt','.rtf','.gif','.tif','.tiff','.xps','.odt','.ods','.odp','.csv']; // Здесь указываем на какую почту администратора должно приходить письмо о новой клиентской заявке: $to = "*******@*******.ru"; // Здесь указываем с какой почты должно приходить письмо: $from = "mail@*******.ru"; // Сюда вписываем название папки в корне сайта, которую мы предварительно создали для загрузки и хранения документов клиента: $uploaddir = 'clients_documents'; // Указываем FTP настройки. Они нужны для формирования ссылки, по которой можно будет скачивать документы клиента из браузера. // В целях безопасности рекомендую создать отдельный ftp-аккаунт, с доступом только к папке с документами. // Т.е. пользователи смогут посмотреть только папку с пришедшими документами + папку на уровень выше (там где будут присланные документы за другие даты) // Т.к. скрипт мы будем использовать только внутри Вашей организации и документы будут приходить на указанный Вами е-мейл (например руководителю отдела по работе с клиентами) // то данная стратегия полностью безопасна. $ftpHost = '81.177.165.34'; // Указываем реальный FTP хост $ftpLogin = 'login'; // Указываем реальный FTP логин $ftpPass = 'password'; // Указываем реальный FTP пароль // У пользователя стандартный FTP доступ (ко всем папкам на сервере) или Вы настроили клиенту доступ только к директории с документами (исходя из рекомендации выше)? // Если вариант 1 - ставим значение true. Если доступ ограничен только текущей папкой - оставляем false $ftpAccess = false; // Задаём переменные, полученные из $_POST. Для безопасности обрезаем все теги, которые можем ввести пользователь if($_SERVER['REQUEST_METHOD'] === 'POST') { $today = date('d-m-Y_H-i-s'); $problem = strip_tags(trim($_POST["problem"])); $object = strip_tags(trim($_POST["object"])); $documents = strip_tags(trim($_POST["documents"])); $dop_info = strip_tags(trim($_POST["dop_info"])); $questions = strip_tags(trim($_POST["questions"])); $name = strip_tags(trim($_POST["name"])); $tel = strip_tags($_POST["tel"]); $email = strip_tags($_POST["email"]); $ooo = strip_tags(trim($_POST["ooo"])); $files = $_FILES; $subject = "У Вас новый запрос на обратный звонок c сайта ****.ru"; // здесь указываем нужную нам тему письма $message = "<h3>Заявка с расширенной формы обратной связи</h3><hr>"."\n"; // здесь указываем заголовок письма // Все поля ниже заполнены из конкретного примера имеющейся ниже формы. Если у Вас будут другие поля, то меняем ниже название переменных // Значения переменных в $_POST['...'] должны соответствовать значениям атрибута name в полях ввода (! это важно !) if(!empty($_POST["problem"])) $message .= "<b>Проблема и цель привлечения экспертной организации:</b><br>{$problem}<hr>"."\n"; if(!empty($_POST["object"])) $message .= "<b>Объект экспертизы:</b><br>{$object}<hr>"."\n"; if(!empty($_POST["documents"])) $message .= "<b>Имеющаяся документация по объекту:</b><br>{$documents}<hr>"."\n"; if(!empty($_POST["dop_info"])) $message .= "<b>Дополнительная информация:</b><br>{$dop_info}<hr>"."\n"; if(!empty($_POST["questions"])) $message .= "<b>Задачи (вопросы) для экспертов:</b><br>{$questions}<hr>"."\n"; if(!empty($_POST["ooo"])) $message .= "<b>Название организации:</b><br>{$ooo}<hr>"."\n"; $message .= "<b>Контактное лицо:</b><br>{$name}<hr>"."\n"."<b>Телефон:</b><br>{$tel}<hr>"."\n"."<b>E-mail:</b><br>{$email}<hr>"."\n"; $extensionArr = []; $filesName = []; // Ниже указывается путь к папке, которую мы задавали выше в переменной $uploaddir // В данном примере, папка расположения данного скрипта и папка 'clients_documents' находятся на одном уровне, в корне сайта, т.е. эти папки находятся на одном уровне. // Если Вы хотите использовать файл с формой как отдельный файл (без папки) и он будет находиться на одном уровне с папкой 'clients_documents', // то тогда строка ниже будет такой: $uploaddir = '/'.$uploaddir.'/'.$today; $uploaddir = '../'.$uploaddir.'/'.$today; // В следующих строчках мы проверяем, загружены ли по факту пользователем файлы. foreach($files as $file) {if(!empty($file['name'])) {array_push($filesName, $file['name']);}} count($filesName)>0 ? $resultUpload = true : $resultUpload = false; // Функция для транслитерации кириллических символов, пробелов и прочего в названии загружаемых файлов function transl($st,$code='utf-8') { $st = mb_strtolower($st, $code); $st = str_replace(array( '?','!',',',':',';','*','(',')','{','}','%','#','№','@','$','^','-','+','/','\\','=','|','"','\'','&','а','б','в','г','д','е','ё','з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ъ','ы','э',' ','ж','ц','ч','ш','щ','ь','ю','я' ), array( '','','','','','','','','','','','','','','','','','','','','','','','','','a','b','v','g','d','e','e','z','i','y','k','l','m','n','o','p','r','s','t','u','f','h','j','i','e','_','zh','ts','ch','sh','shch','','yu','ya' ), $st); return $st; } // Если на каком-то этапе понадобится отладка - раскомментрируйте строку ниже // echo "<pre>"; print_r($files); echo "</pre>"; if($resultUpload) { // Если файлы были загружены if(!is_dir($uploaddir)) mkdir($uploaddir, 0755, true); // проверяем, была ли создана директория для загрузки файлов foreach($files as $file) { // перебираем массив с файлами if(!empty($file['name'])) { // Проверяем каждый файл на соответствие "хорошим" расширениям, которые мы задавали в настройках выше if(in_array(strrchr(mb_strtolower($file['name']),'.'), $goodFiles)) { // Перемещаем файлы из временной папки сервера, одновременно производим транслитерацию if(move_uploaded_file($file['tmp_name'], $uploaddir.'/'.transl($file['name']))) { $files[] = realpath($uploaddir.$file['name']); // для безопасности сразу всем файлам делаем права только на чтение chmod($uploaddir.'/'.transl($file['name']), 0444); } } else { // Если была попытка загрузки файла с "плохим" расширением $loadError = true; $extension = strrchr($file['name'],'.'); array_push($extensionArr, $file['name']); // формируем массив с "плохими" расширениями echo "<style>b{font-weight:bold!important}</style>"; // выводим сообщение пользователю echo "<b>{$file['name']}</b> - этот файл не загружен, т.к. запрещено загружать файлы с расширением <b>{$extension}</b><br>"; } } } if($loadError) { echo "<br>Все остальные файлы успешно загружены!"; $badExtensions = implode("\n", $extensionArr); $htmlStart = '<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><body><pre><h2>'; $htmlEnd = '</h2></pre></body></html>'; // формируем файл в папке с загруженными файлами, что была попытка загрузки файлов с "плохими" расширениями и указываем какие конкретно это файлы FILE_put_contents($uploaddir.'/info.txt', $htmlStart."Клиент пытался загрузить запрещённые файлы:\n{$badExtensions}".$htmlEnd, FILE_APPEND); } // ниже формируем строку с ссылкой для браузера, чтобы можно было посмотреть и скачать файлы клиента if(!$ftpAccess) { $message .= "<b>Путь на файлы, отправленные клиентом:</b><br>"."ftp://{$ftpLogin}:{$ftpPass}@{$ftpHost}".'/'.$today."/<hr>"."\n"; } else { $message .= "<b>Путь на файлы, отправленные клиентом:</b><br>"."ftp://{$ftpLogin}:{$ftpPass}@{$ftpHost}".strstr($uploaddir,'/')."/<hr>"."\n"; } } $subject = "=?utf-8?B?".base64_encode($subject)."?="; $headers = "From: $from\r\nReply-to: $from\r\nContent-type: text/html; charset=utf-8\r\n"; // Ниже формируем логи, в которые пишем всю информацию, введённую клиентом, а также информацию о попытке загрузки "плохих" файлов $logText = strip_tags($message); $logFile = strstr($uploaddir, '/'.$today, true)."/mail.log"; FILE_put_contents($logFile, "\n{$today}\n{$logText}\n", FILE_APPEND); if($loadError) FILE_put_contents($logFile, "{$today} была попытка загрузки запрещённых файлов:\n{$badExtensions}\n", FILE_APPEND); chmod($logFile, 0600); if(mail($to, $subject, $message, $headers)) { // отправляем письмо администратору с данными из формы, заполненной клиентом $subj = "Ваша заявка принята в работу в ****************"; // вместо звёздочек ставим название организации $subj = "=?utf-8?B?".base64_encode($subj)."?="; $mess = "{$name}, Ваша заявка принята в работу в ******************.\n<br>Ожидайте звонок на номер телефона {$tel} в течении 8 рабочих часов.\n\n<br><br>С уважением,\n<br>НАЗВАНИЕ КОМПАНИИ\n<br>+7 (***) ***-**-**\n<br>+7 (***) ***-**-**\n<br>https://www.*******.ru"; mail($email, $subj, $mess, $headers); // дополнительно отправляем письмо клиенту о том, что его заявка принята в работу echo "<div class='general_form_sended'><b>{$name}</b>, Ваша заявка принята в работу в **************.<br>Ожидайте звонок на номер телефона {$tel} в течении 8 рабочих часов. Письмо с нашими контактными данными направлено на Ваш e-mail {$email}</div>"; echo "<form class='hidden'>"; } } else { echo "<form action='#' class='general_form' method='POST' enctype='multipart/form-data'>"; } ?> <!-- Все поля ввода, приведённые ниже, указаны в качестве примера --> <div> <p>Кратко опишите проблему и цель привлечения экспертной организации</p> <textarea rows="5" name="problem"></textarea> </div> <div> <p>Кратко опишите объект (местонахождение, площадь, объем, этажность, протяженность, проект, раздел проекта, отдельная конструкция, строительные материалы, сметная документация, исходно-разрешительная документация, бизнес-планы, нематериальные активы и т.п.)</p> <textarea rows="5" name="object"></textarea> </div> <div> <p>Перечислите имеющуюся документацию по объекту (проектно-сметная документация, технологический паспорт, договоры, приложения, планы и т.п.)</p> <textarea rows="5" name="documents"></textarea> </div> <div> <p>Если у Вас имеется дополнительная информация, в том числе и фотографии, укажите её здесь</p> <textarea rows="5" name="dop_info"></textarea> </div> <div> <p>Сформулируйте задачи (вопросы), которые Вы хотите поставить на разрешение экспертов</p> <textarea rows="5" name="questions"></textarea> </div> <div> <p>Прикрепите файлы (Вы можете прикрепить к сообщению до 10-ти файлов (включительно))</p> <p>Разрешённые типы файлов для загрузки: <?=implode(", ",$goodFiles)?></p> <br> <input type="file" name="file0"><input type="file" name="file1"> <input type="file" name="file2"><input type="file" name="file3"> <input type="file" name="file4"><input type="file" name="file5"> <input type="file" name="file6"><input type="file" name="file7"> <input type="file" name="file8"><input type="file" name="file9"> </div> <fieldset> <legend>Пожалуйста, уточните контактную информацию для связи с Вами</legend> <div> <p>Контактное лицо *</p> <input name="name" required pattern="^[а-яА-ЯёЁ\s]+$"> </div> <div> <p>Телефон *</p> <input name="tel" type="tel" required> </div> <div> <p>E-mail *</p> <input name="email" type="email" required> </div> <div> <p>Название организации</p> <input name="ooo"> </div> </fieldset> <button type="submit">Отправить заявку</button> </form> ?>
СТИЛИ:
.general_form, .general_form * {-webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box;} .general_form {margin-top: 20px; margin-bottom: 30px;} .general_form legend, .general_form fieldset {text-align: center;} .general_form fieldset {margin-top: 40px; background: rgba(17, 61, 124, .05);} .general_form fieldset > div {width: 90%; margin-left: 5%; margin-bottom: 15px;} .general_form fieldset > div:last-child {margin-bottom: 20px;} .general_form p {margin-bottom: 3px;} .general_form fieldset p {margin-bottom: 0;} .general_form input {line-height: 25px; padding-top: 2px !important; padding-bottom: 2px !important;} .general_form textarea, .general_form input:not([type="file"]) {width: 100%; border-radius: 4px; border: 1px solid rgba(17, 61, 124, 0.5); box-shadow: 0 0 1px #113d7c; padding: 10px;} .general_form input[type="file"] {display: inline-block; width: 50%; float: left;} .general_form > div {margin-bottom: 20px; overflow: hidden;} .general_form button {display: block; margin: 25px auto 35px; padding: 10px 30px; cursor: pointer; background: #113D7C; color: #fff; border: 3px solid #fff; border-radius: 5px; font-size: 18px; transition: box-shadow .25s;} .general_form button:hover {box-shadow: 0 0 2px 2px #113d7c;} .general_form_sended {margin-top: 20px; margin-bottom: 30px; padding: 20px; border-radius: 5px; border: 2px solid green; font-size: 17px; line-height: 1.35;} .general_form_sended b {font-weight: bold !important;} @media(max-width: 767px) {.general_form input[type="file"] {width: 100%; margin-bottom: 10px;}}
СКРИПТЫ:
Они нужны только для эффектов и отправки события в Метрику. Вы можете не использовать их.
Если всё же захотите использовать, то на Вашем сайте должна стоять библиотека jQuery.
$('form.general_form').on('submit', function() {yaCounter********.reachGoal('general_form');}); // здесь прописываем номер счётчика на Яндекс.Метрике. Если не требуется - удаляем эту строчку полностью $('form.general_form textarea, form.general_form input').on('focus', function() { $('form.general_form p').css({'font-weight':'normal', 'color':'#000', 'letter-spacing': 'normal'}); $(this).prev('p').css({'font-weight':'bold', 'color':'#113D7C', 'letter-spacing': '-.55px'}); if(this.tagName=='TEXTAREA'){$('form.general_form textarea').css('background','white');$(this).css('background','aliceblue')}; })
Подписывайтесь на группу в ВКонтакте, вступайте в сообщество на Facebook, чтобы всегда быть в курсе актуальных выпусков
Web development blog!