Недавно я делал форму обратной связи с возможностью загрузки файлов. При этом стояла задача сделать форму идеально безопасной, с "белым списком" файлов, а также на выходе формировать письмо клиентскому отделу с данными клиента и всеми его файлами. Поскольку файлы (согласно ТЗ) могут иметь большой размер (до 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!