[TASK] updated contact-form-layout

This commit is contained in:
TLRZ Seyfferth 2020-01-29 14:09:22 +01:00
parent 571576756e
commit 8675176bb6
2 changed files with 362 additions and 293 deletions

View File

@ -1,17 +1,16 @@
<?php <?php
/** /**
* p01-contact - A simple contact forms manager * p01-contact - A simple contact forms manager.
*
* @see https://github.com/nliautaud/p01contact
* *
* @link https://github.com/nliautaud/p01contact
* @author Nicolas Liautaud * @author Nicolas Liautaud
* @package p01contact
*/ */
namespace P01C; namespace P01C;
class P01contactField class P01contactField
{ {
private $form;
public $id; public $id;
public $type; public $type;
public $title; public $title;
@ -22,6 +21,7 @@ class P01contactField
public $required; public $required;
public $locked; public $locked;
public $error; public $error;
private $form;
/** /**
* @param Form $form the container form * @param Form $form the container form
@ -36,7 +36,7 @@ class P01contactField
} }
/** /**
* Set the field value or selected value * Set the field value or selected value.
* *
* @param mixed $new_value the value, or an array of selected values ids * @param mixed $new_value the value, or an array of selected values ids
*/ */
@ -45,11 +45,12 @@ class P01contactField
// simple value // simple value
if (!is_array($this->value)) { if (!is_array($this->value)) {
$this->value = htmlentities($new_value, ENT_COMPAT, 'UTF-8', false); $this->value = htmlentities($new_value, ENT_COMPAT, 'UTF-8', false);
return; return;
} }
// multiples-values (checkbox, radio, select) // multiples-values (checkbox, radio, select)
if (!is_array($new_value)) { if (!is_array($new_value)) {
$new_value = array($new_value); $new_value = [$new_value];
} }
foreach ($new_value as $i) { foreach ($new_value as $i) {
$this->selected_values[intval($i)] = true; $this->selected_values[intval($i)] = true;
@ -57,11 +58,11 @@ class P01contactField
} }
/** /**
* Reset the selected values by finding ones who starts or end with ":" * Reset the selected values by finding ones who starts or end with ":".
*/ */
public function resetSelectedValues() public function resetSelectedValues()
{ {
$this->selected_values = array(); $this->selected_values = [];
foreach ($this->value as $i => $val) { foreach ($this->value as $i => $val) {
$value = preg_replace('`(^\s*:|:\s*$)`', '', $val, -1, $count); $value = preg_replace('`(^\s*:|:\s*$)`', '', $val, -1, $count);
if ($count) { if ($count) {
@ -73,32 +74,38 @@ class P01contactField
/** /**
* Check field value. * Check field value.
* @return boolean *
* @return bool
*/ */
public function validate() public function validate()
{ {
// empty and required // empty and required
if (empty($this->value) && $this->required) { if (empty($this->value) && $this->required) {
$this->error = 'field_required'; $this->error = 'field_required';
return false; return false;
} }
// value blacklisted or not in whitelist // value blacklisted or not in whitelist
if ($reason = $this->isBlacklisted()) { if ($reason = $this->isBlacklisted()) {
$this->error = 'field_'.$reason; $this->error = 'field_'.$reason;
return false; return false;
} }
// not empty but not valid // not empty but not valid
if (!empty($this->value) && !$this->isValid()) { if (!empty($this->value) && !$this->isValid()) {
$this->error = 'field_'.$this->type; $this->error = 'field_'.$this->type;
return false; return false;
} }
return true; return true;
} }
/** /**
* Check if field value is valid * Check if field value is valid
* Mean different things depending on field type * Mean different things depending on field type.
* @return boolean *
* @return bool
*/ */
public function isValid() public function isValid()
{ {
@ -107,6 +114,7 @@ class P01contactField
return filter_var($this->value, FILTER_VALIDATE_EMAIL); return filter_var($this->value, FILTER_VALIDATE_EMAIL);
case 'tel': case 'tel':
$pattern = '`^\+?[-0-9(). ]{6,}$$`i'; $pattern = '`^\+?[-0-9(). ]{6,}$$`i';
return preg_match($pattern, $this->value); return preg_match($pattern, $this->value);
case 'url': case 'url':
return filter_var($this->value, FILTER_VALIDATE_URL); return filter_var($this->value, FILTER_VALIDATE_URL);
@ -122,8 +130,11 @@ class P01contactField
} }
/** /**
* Check if reCaptcha is valid * Check if reCaptcha is valid.
* @return boolean *
* @param mixed $answer
*
* @return bool
*/ */
public function reCaptchaValidity($answer) public function reCaptchaValidity($answer)
{ {
@ -132,9 +143,9 @@ class P01contactField
} }
$params = [ $params = [
'secret' => $this->form->config('recaptcha_secret_key'), 'secret' => $this->form->config('recaptcha_secret_key'),
'response' => $answer 'response' => $answer,
]; ];
$url = "https://www.google.com/recaptcha/api/siteverify?" . http_build_query($params); $url = 'https://www.google.com/recaptcha/api/siteverify?'.http_build_query($params);
if (function_exists('curl_version')) { if (function_exists('curl_version')) {
$curl = curl_init($url); $curl = curl_init($url);
curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_HEADER, false);
@ -154,12 +165,12 @@ class P01contactField
} }
/** /**
* Check if field value is blacklisted * Check if field value is blacklisted.
* *
* Search for every comma-separated entry of every checklist * Search for every comma-separated entry of every checklist
* in value, and define if it should or should not be there. * in value, and define if it should or should not be there.
* *
* @return boolean * @return bool
*/ */
public function isBlacklisted() public function isBlacklisted()
{ {
@ -173,14 +184,15 @@ class P01contactField
} }
$content = array_filter(explode(',', $cl->content)); $content = array_filter(explode(',', $cl->content));
foreach ($content as $avoid) { foreach ($content as $avoid) {
$found = preg_match("`$avoid`", $this->value); $found = preg_match("`{$avoid}`", $this->value);
$foundBlacklisted = $found && $cl->type == 'blacklist'; $foundBlacklisted = $found && 'blacklist' == $cl->type;
$notFoundWhitelisted = !$found && $cl->type == 'whitelist'; $notFoundWhitelisted = !$found && 'whitelist' == $cl->type;
if ($foundBlacklisted || $notFoundWhitelisted) { if ($foundBlacklisted || $notFoundWhitelisted) {
return $cl->type; return $cl->type;
} }
} }
} }
return false; return false;
} }
@ -201,71 +213,80 @@ class P01contactField
$required = $this->required ? ' required ' : ''; $required = $this->required ? ' required ' : '';
$placeholder = $this->placeholder ? ' placeholder="'.$this->placeholder.'"' : ''; $placeholder = $this->placeholder ? ' placeholder="'.$this->placeholder.'"' : '';
$is_single_option = is_array($this->value) && count($this->value) == 1; $is_single_option = is_array($this->value) && 1 == count($this->value) ? 'inline' : '';
if ($is_single_option) { $html = "<div class=\"row field {$is_single_option} {$type} {$orig} {$required}\">";
$html = "<div class=\"field inline $type $orig $required\">";
} else { $html .= '<div class="col-sm-12 col-md-3">';
$html = "<div class=\"field $type $orig $required\">"; if ('' === $is_single_option) {
$html .= $this->htmlLabel($id); $html .= $this->htmlLabel($id);
} }
$html .= '</div>';
$html .= '<div class="col-sm-12 col-md">';
switch ($type) { switch ($type) {
case 'textarea': case 'textarea':
$html .= '<textarea id="'.$id.'" rows="10" '; $html .= '<textarea id="'.$id.'" rows="10" ';
$html .= 'name="'.$name.'"'.$disabled.$required.$placeholder; $html .= 'name="'.$name.'"'.$disabled.$required.$placeholder;
$html .= '>'.$value.'</textarea>'; $html .= '>'.$value.'</textarea>';
break; break;
case 'captcha': case 'captcha':
$key = $this->form->config('recaptcha_public_key'); $key = $this->form->config('recaptcha_public_key');
if (!$key) { if (!$key) {
break; break;
} }
if ($this->form->getId() == 1) { if (1 == $this->form->getId()) {
$html .= '<script src="https://www.google.com/recaptcha/api.js"></script>'; $html .= '<script src="https://www.google.com/recaptcha/api.js"></script>';
} }
$html .= '<div class="g-recaptcha" id="'.$id.'" data-sitekey="'.$key.'"></div>'; $html .= '<div class="g-recaptcha" id="'.$id.'" data-sitekey="'.$key.'"></div>';
$html .="<input type=\"hidden\" id=\"$id\" name=\"$name\" value=\"trigger\">"; $html .= "<input type=\"hidden\" id=\"{$id}\" name=\"{$name}\" value=\"trigger\">";
break; break;
case 'checkbox': case 'checkbox':
case 'radio': case 'radio':
$html .= '<div class="options">'; $html .= '<div class="options row">';
foreach ($this->value as $i => $v) { foreach ($this->value as $i => $v) {
$selected = $this->isSelected($i) ? ' checked' : ''; $selected = $this->isSelected($i) ? ' checked' : '';
$v = !empty($v) ? $v : 'Default'; $v = !empty($v) ? $v : 'Default';
$html .= '<label class="option">'; $html .= '<label class="option col-sm-12">';
$html .= "<input id=\"{$id}_option{$i}\""; $html .= "<input id=\"{$id}_option{$i}\"";
$html .= " type=\"$type\" class=\"$type\" name=\"{$name}\""; $html .= " type=\"{$type}\" class=\"{$type}\" name=\"{$name}\"";
$html .= " value=\"$i\"$disabled$required$selected />$v"; $html .= " value=\"{$i}\"{$disabled}{$required}{$selected} />{$v}";
$html .= '</label>'; $html .= '</label>';
} }
$html .= '</div>'; $html .= '</div>';
break; break;
case 'select': case 'select':
$html .= "<select id=\"$id\" name=\"$name\"$disabled$required>"; $html .= "<select id=\"{$id}\" name=\"{$name}\"{$disabled}{$required}>";
foreach ($this->value as $i => $v) { foreach ($this->value as $i => $v) {
$value = !empty($v) ? $v : 'Default'; $value = !empty($v) ? $v : 'Default';
$selected = $this->isSelected($i) ? ' selected="selected"' : ''; $selected = $this->isSelected($i) ? ' selected="selected"' : '';
$html .= "<option id=\"{$id}_option{$i}\" value=\"$i\"$selected>"; $html .= "<option id=\"{$id}_option{$i}\" value=\"{$i}\"{$selected}>";
$html .= $value.'</option>'; $html .= $value.'</option>';
} }
$html .= '</select>'; $html .= '</select>';
break; break;
default: default:
$html .= '<input id="'.$id.'" '; $html .= '<input id="'.$id.'" ';
$html .= 'name="'.$name.'" type="'.$type.'" '; $html .= 'name="'.$name.'" type="'.$type.'" ';
$html .= 'value="'.$value.'"'.$disabled.$required.$placeholder.' />'; $html .= 'value="'.$value.'"'.$disabled.$required.$placeholder.' />';
break; break;
} }
$html .= '</div>'; $html .= '</div>';
$html .= '</div>';
return $html; return $html;
} }
/*
* Return a html presentation of the field value. // Return a html presentation of the field value.
*/
public function htmlMail() public function htmlMail()
{ {
$gen_type = $this->getGeneralType(); $gen_type = $this->getGeneralType();
$properties = array(); $properties = [];
$html = '<table style="width: 100%; margin: 1em 0; border-collapse: collapse;">'; $html = '<table style="width: 100%; margin: 1em 0; border-collapse: collapse;">';
@ -289,8 +310,8 @@ class P01contactField
if ($gen_type != $this->type) { if ($gen_type != $this->type) {
$properties[] = $gen_type; $properties[] = $gen_type;
} }
foreach (array('locked', 'required') as $property) { foreach (['locked', 'required'] as $property) {
if ($this->$property) { if ($this->{$property}) {
$properties[] = $this->form->lang($property); $properties[] = $this->form->lang($property);
} }
} }
@ -323,15 +344,18 @@ class P01contactField
$html .= empty($v) ? 'Default' : $v; $html .= empty($v) ? 'Default' : $v;
$html .= "</div>\n"; $html .= "</div>\n";
} }
break; break;
default: default:
$address = '~[[:alpha:]]+://[^<>[:space:]]+[[:alnum:]/]~'; $address = '~[[:alpha:]]+://[^<>[:space:]]+[[:alnum:]/]~';
$val = nl2br(preg_replace($address, '<a href="\\0">\\0</a>', $this->value)); $val = nl2br(preg_replace($address, '<a href="\\0">\\0</a>', $this->value));
$html .= "<p style=\"margin:0\">$val</p>"; $html .= "<p style=\"margin:0\">{$val}</p>";
break; break;
} }
$html .= '</div></td></tr></table>'; $html .= '</div></td></tr></table>';
return $html; return $html;
} }
@ -347,7 +371,7 @@ class P01contactField
*/ */
private function htmlLabel($for) private function htmlLabel($for)
{ {
$html = '<label for="' . $for . '">'; $html .= '<label for="'.$for.'" class="doc">';
if ($this->title) { if ($this->title) {
$html .= $this->title; $html .= $this->title;
} else { } else {
@ -360,6 +384,7 @@ class P01contactField
$html .= ' <span class="error-msg">'.$this->form->lang($this->error).'</span>'; $html .= ' <span class="error-msg">'.$this->form->lang($this->error).'</span>';
} }
$html .= '</label>'; $html .= '</label>';
return $html; return $html;
} }
@ -368,15 +393,16 @@ class P01contactField
*/ */
private function getGeneralType() private function getGeneralType()
{ {
$types = array( $types = [
'name' => 'text', 'name' => 'text',
'subject' => 'text', 'subject' => 'text',
'message' => 'textarea', 'message' => 'textarea',
'askcopy' => 'checkbox' 'askcopy' => 'checkbox',
); ];
if (isset($types[$this->type])) { if (isset($types[$this->type])) {
return $types[$this->type]; return $types[$this->type];
} }
return $this->type; return $this->type;
} }
} }
@ -402,5 +428,6 @@ function unset_r($a, $i)
unset($a[$k][$i]); unset($a[$k][$i]);
} }
} }
return $a; return $a;
} }

View File

@ -1,25 +1,26 @@
<?php <?php
/** /**
* p01-contact - A simple contact forms manager * p01-contact - A simple contact forms manager.
*
* @see https://github.com/nliautaud/p01contact
* *
* @link https://github.com/nliautaud/p01contact
* @author Nicolas Liautaud * @author Nicolas Liautaud
* @package p01contact
*/ */
namespace P01C; namespace P01C;
require 'P01contact_Field.php'; require 'P01contact_Field.php';
class P01contactForm class P01contactForm
{ {
public $lang;
public $sent;
private $manager; private $manager;
private $id; private $id;
private $status; private $status;
private $targets; private $targets;
private $fields; private $fields;
public $lang;
public $sent;
/** /**
* @param P01contact $P01contact * @param P01contact $P01contact
@ -28,14 +29,14 @@ class P01contactForm
public function __construct($P01contact) public function __construct($P01contact)
{ {
static $id; static $id;
$id++; ++$id;
$this->manager = $P01contact; $this->manager = $P01contact;
$this->id = $id; $this->id = $id;
$this->status = ''; $this->status = '';
$this->targets = array(); $this->targets = [];
$this->fields = array(); $this->fields = [];
} }
/** /**
@ -77,10 +78,324 @@ class P01contactForm
$this->addTarget($email); $this->addTarget($email);
} }
} }
/** /**
* Create a field by parsing a tag parameter * Update POSTed form and try to send mail.
*
* Check posted data, update form data,
* define fields errors and form status.
* At least, if there is no errors, try to send mail.
*/
public function post()
{
if (empty($_POST['p01-contact_form'])
|| $_POST['p01-contact_form']['id'] != $this->id) {
return;
}
// check token
if (!$this->checkToken()) {
$this->setStatus('sent_already');
$this->setToken();
$this->reset();
return;
}
$posted = $_POST['p01-contact_fields'];
// populate fields values and check errors
$hasFieldsErrors = false;
$fields = $this->getFields();
foreach ($fields as $field) {
if (!isset($posted[$field->id])) {
continue;
}
$posted_val = $posted[$field->id];
$field->setValue($posted_val);
$hasFieldsErrors = !$field->validate() || $hasFieldsErrors;
}
// check errors and set status
if ($this->config('disable')) {
$this->setStatus('disable');
return;
}
if (0 == count($this->targets)) {
$this->setStatus('error_notarget');
return;
}
if ($hasFieldsErrors || true !== $this->checkSpam($posted)) {
return;
}
$this->sendMail();
$this->setToken();
$this->reset();
}
/**
* Get the token in Session (create it if not exists).
*
* @return string
*/
public function getToken()
{
if (!Session::get('token', false)) {
$this->setToken();
}
return Session::get('token');
}
// RENDER
/**
* Return the html display of the form.
*
* @return string the <form>
*/
public function html()
{
$html = '<div class="section">';
$html .= '<form action="'.PAGEURL.'#p01-contact'.$this->id.'" autocomplete="off" ';
$html .= 'id="p01-contact'.$this->id.'" class="p01-contact" method="post">';
if ($this->status) {
$html .= $this->htmlStatus();
}
if (!$this->sent) {
foreach ($this->fields as $field) {
$html .= $field->html();
}
if ($this->config('use_honeypot')) {
$html .= '<input type="checkbox" name="p01-contact_fields[totally_legit]" value="1" style="display:none !important" tabindex="-1" autocomplete="false">';
}
$html .= '<div><input name="p01-contact_form[id]" type="hidden" value="'.$this->id.'" />';
$html .= '<input name="p01-contact_form[token]" type="hidden" value="'.$this->getToken().'" />';
$html .= '<input class="submit" type="submit" value="'.$this->lang('send').'" /></div>';
}
$html .= '</form>';
$html .= '</div>';
if ($this->config('debug')) {
$html .= $this->debug(false);
}
return $html;
}
/**
* Return P01contact_form infos.
*
* @param mixed $set_infos
*
* @return string
*/
public function debug($set_infos)
{
$out = '<div class="debug debug_form">';
static $post;
if ($set_infos) {
$post = $set_infos;
return;
}
if ($post) {
list($headers, $targets, $subject, $text_content, $html_content) = $post;
$out .= '<h3>Virtually sent mail :</h3>';
$out .= '<pre>'.htmlspecialchars($headers).'</pre>';
$out .= "<pre>Targets: {$targets}\nSubject: {$subject}</pre>";
$out .= "Text content : <pre>{$text_content}</pre>";
$out .= "HTML content : <div style=\"border:1px solid #ccc;\">{$html_content}</div>";
}
$infos = $this;
unset($infos->manager);
$out .= "<h3>p01contact form {$this->id} :</h3>";
$out .= preint($infos, true);
$out .= '</div>';
return $out;
}
// MAIL
/**
* Send a mail based on form.
*
* Create the mail content and headers along to settings, form
* and fields datas; and update the form status (sent|error).
*/
public function sendMail()
{
$email = $name = $subject = $askcopy = null;
$tpl_data = (object) null;
$tpl_data->date = date('r');
$tpl_data->ip = $_SERVER['REMOTE_ADDR'];
$tpl_data->contact = $this->targets[0];
// fields
$tpl_data->fields = '';
foreach ($this->fields as $field) {
$tpl_data->fields .= $field->htmlMail();
switch ($field->type) {
case 'name':
$name = $field->value;
break;
case 'email':
$email = $field->value;
break;
case 'subject':
$subject = $field->value;
break;
case 'askcopy':
$askcopy = true;
break;
}
}
$html = $this->manager->renderTemplate('mail_template', $tpl_data);
$text = strip_tags($html);
if (empty($name)) {
$name = $this->lang('anonymous');
}
if (empty($subject)) {
$subject = $this->lang('nosubject');
}
// targets, subject, headers and multipart content
$targets = implode(',', $this->targets);
$encoded_subject = $this->encodeHeader($subject);
$mime_boundary = '----'.md5(time());
$headers = $this->mailHeaders($name, $email, $mime_boundary);
$content = $this->mailContent($text, 'plain', $mime_boundary);
$content .= $this->mailContent($html, 'html', $mime_boundary);
$content .= "--{$mime_boundary}--\n\n";
// debug
if ($this->config('debug')) {
$this->debug([$headers, $targets, $subject, $text, $html]);
return $this->setStatus('sent_debug');
}
// send mail
$success = mail($targets, $encoded_subject, $content, $headers);
// log
$this->manager->log([
date('d/m/Y H:i:s'), $targets, $subject, $name, $success ? 'success' : 'error',
]);
if (!$success) {
return $this->setStatus('error');
}
if (!$email || !$askcopy) {
return $this->setStatus('sent');
}
// mail copy
$copy = mail($email, $encoded_subject, $content, $headers);
$this->setStatus($copy ? 'sent_copy' : 'sent_copy_error');
}
/**
* Return array of valid emails from a comma separated string.
*
* @param string $emails
*
* @return array
*/
public static function getValidEmails($emails)
{
return array_filter(explode(',', $emails), function ($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
});
}
/**
* GETTERS / SETTERS.
*/
// Reset all fields values and errors
public function reset()
{
foreach ($this->fields as $field) {
$field->value = '';
$field->error = '';
}
}
public function getTargets()
{
return $this->targets;
}
public function addTarget($tget)
{
if (false === in_array($tget, $this->targets)) {
$this->targets[] = $tget;
}
}
public function getField($id)
{
return $this->fields[$id];
}
public function getFields()
{
return $this->fields;
}
public function addField($field)
{
$this->fields[] = $field;
}
public function getStatus()
{
return $this->status;
}
public function setStatus($status)
{
if (!is_string($status)) {
return;
}
$this->status = $status;
if ('sent' == substr($status, 0, 4)) {
$this->sent = true;
}
}
public function getId()
{
return $this->id;
}
public function config($key)
{
return $this->manager->config($key);
}
public function lang($key)
{
return $this->manager->lang($key, $this->lang);
}
/**
* Create a field by parsing a tag parameter.
* *
* Find emails and parameters, create and setup form object. * Find emails and parameters, create and setup form object.
*
* @param int $id the field id * @param int $id the field id
* @param string $param the param to parse * @param string $param the param to parse
*/ */
@ -105,17 +420,20 @@ class P01contactForm
case 'checkbox': case 'checkbox':
$field->value = explode('|', $values); $field->value = explode('|', $values);
$field->resetSelectedValues(); $field->resetSelectedValues();
break; break;
case 'askcopy': case 'askcopy':
// checkbox-like structure // checkbox-like structure
$field->value = array($this->lang('askcopy')); $field->value = [$this->lang('askcopy')];
break; break;
case 'password': case 'password':
// password value is required value // password value is required value
$field->required = $values; $field->required = $values;
break; break;
default: default:
if ($assign == '=<') { if ('=<' == $assign) {
$field->placeholder = $values; $field->placeholder = $values;
} else { } else {
// simple value // simple value
@ -123,78 +441,20 @@ class P01contactForm
} }
} }
// required // required
if ($type != 'password') { if ('password' != $type) {
$field->required = $required == '!'; $field->required = '!' == $required;
} }
if ($type == 'captcha') { if ('captcha' == $type) {
$field->required = true; $field->required = true;
} }
$field->title = $title; $field->title = $title;
$field->description = $desc; $field->description = $desc;
$field->locked = $assign == '=>'; $field->locked = '=>' == $assign;
$this->addField($field); $this->addField($field);
} }
/** // SECURITY
* Update POSTed form and try to send mail
*
* Check posted data, update form data,
* define fields errors and form status.
* At least, if there is no errors, try to send mail.
*/
public function post()
{
if (empty($_POST['p01-contact_form'])
|| $_POST['p01-contact_form']['id'] != $this->id ) {
return;
}
// check token
if (!$this->checkToken()) {
$this->setStatus('sent_already');
$this->setToken();
$this->reset();
return;
}
$posted = $_POST['p01-contact_fields'];
// populate fields values and check errors
$hasFieldsErrors = false;
$fields = $this->getFields();
foreach ($fields as $field) {
if (!isset($posted[$field->id])) {
continue;
}
$posted_val = $posted[$field->id];
$field->setValue($posted_val);
$hasFieldsErrors = !$field->validate() || $hasFieldsErrors;
}
// check errors and set status
if ($this->config('disable')) {
$this->setStatus('disable');
return;
}
if (count($this->targets) == 0) {
$this->setStatus('error_notarget');
return;
}
if ($hasFieldsErrors || $this->checkSpam($posted) !== true) {
return;
}
$this->sendMail();
$this->setToken();
$this->reset();
}
/*
* SECURITY
*/
/** /**
* Check if the honeypot field is untouched and if the time between this post, * Check if the honeypot field is untouched and if the time between this post,
@ -203,27 +463,32 @@ class P01contactForm
* *
* @param P01contact_form $form The submitted form * @param P01contact_form $form The submitted form
* @param array $post Sanitized p01-contact data of $_POST * @param array $post Sanitized p01-contact data of $_POST
*
* @return bool the result status * @return bool the result status
*/ */
private function checkSpam($post) private function checkSpam($post)
{ {
if (isset($post['totally_legit'])) { if (isset($post['totally_legit'])) {
$this->setStatus('error_honeypot'); $this->setStatus('error_honeypot');
return false; return false;
} }
$loads = Session::get('pageloads'); $loads = Session::get('pageloads');
if (count($loads) > 1 && $loads[1] - $loads[0] < $this->config('min_sec_after_load')) { if (count($loads) > 1 && $loads[1] - $loads[0] < $this->config('min_sec_after_load')) {
$this->setStatus('error_pageload'); $this->setStatus('error_pageload');
return false; return false;
} }
$lastpost = Session::get('lastpost', false); $lastpost = Session::get('lastpost', false);
if ($lastpost && time() - $lastpost < $this->config('min_sec_between_posts')) { if ($lastpost && time() - $lastpost < $this->config('min_sec_between_posts')) {
$this->setStatus('error_lastpost'); $this->setStatus('error_lastpost');
return false; return false;
} }
$postcount = Session::get('postcount', 0); $postcount = Session::get('postcount', 0);
if (!$this->config('debug') && $postcount > $this->config('max_posts_by_hour')) { if (!$this->config('debug') && $postcount > $this->config('max_posts_by_hour')) {
$this->setStatus('error_postcount'); $this->setStatus('error_postcount');
return false; return false;
} }
@ -234,312 +499,89 @@ class P01contactForm
} }
/** /**
* Create an unique hash in Session * Create an unique hash in Session.
*/ */
private static function setToken() private static function setToken()
{ {
Session::set('token', uniqid(md5(microtime()), true)); Session::set('token', uniqid(md5(microtime()), true));
} }
/** /**
* Get the token in Session (create it if not exists) * Compare the POSTed token to the Session one.
* @return string *
*/ * @return bool
public function getToken()
{
if (!Session::get('token', false)) {
$this->setToken();
}
return Session::get('token');
}
/**
* Compare the POSTed token to the Session one
* @return boolean
*/ */
private function checkToken() private function checkToken()
{ {
return $this->getToken() === $_POST['p01-contact_form']['token']; return $this->getToken() === $_POST['p01-contact_form']['token'];
} }
/*
* RENDER
*/
/** /**
* Return the html display of the form * Return an html display of the form status.
* @return string the <form> *
*/
public function html()
{
$html = '<form action="'.PAGEURL.'#p01-contact'.$this->id.'" autocomplete="off" ';
$html .= 'id="p01-contact' . $this->id . '" class="p01-contact" method="post">';
if ($this->status) {
$html .= $this->htmlStatus();
}
if (!$this->sent) {
foreach ($this->fields as $field) {
$html .= $field->html();
}
if ($this->config('use_honeypot')) {
$html .= '<input type="checkbox" name="p01-contact_fields[totally_legit]" value="1" style="display:none !important" tabindex="-1" autocomplete="false">';
}
$html .= '<div><input name="p01-contact_form[id]" type="hidden" value="' . $this->id . '" />';
$html .= '<input name="p01-contact_form[token]" type="hidden" value="' . $this->getToken() . '" />';
$html .= '<input class="submit" type="submit" value="' . $this->lang('send') . '" /></div>';
}
$html .= '</form>';
if ($this->config('debug')) {
$html .= $this->debug(false);
}
return $html;
}
/**
* Return an html display of the form status
* @return string the <div> * @return string the <div>
*/ */
private function htmlStatus() private function htmlStatus()
{ {
$statusclass = $this->sent ? 'alert success' : 'alert failed'; $statusclass = $this->sent ? 'alert success' : 'alert failed';
return '<div class="'.$statusclass.'">'.$this->lang($this->status).'</div>'; return '<div class="'.$statusclass.'">'.$this->lang($this->status).'</div>';
} }
/** /**
* Return P01contact_form infos. * Return the mail headers.
* @return string
*/
public function debug($set_infos)
{
$out = '<div class="debug debug_form">';
static $post;
if ($set_infos) {
$post = $set_infos;
return;
}
if ($post) {
list($headers, $targets, $subject, $text_content, $html_content) = $post;
$out.= '<h3>Virtually sent mail :</h3>';
$out.= '<pre>'.htmlspecialchars($headers).'</pre>';
$out.= "<pre>Targets: $targets\nSubject: $subject</pre>";
$out.= "Text content : <pre>$text_content</pre>";
$out.= "HTML content : <div style=\"border:1px solid #ccc;\">$html_content</div>";
}
$infos = $this;
unset($infos->manager);
$out .= "<h3>p01contact form $this->id :</h3>";
$out .= preint($infos, true);
$out .= '</div>';
return $out;
}
/*
* MAIL
*/
/**
* Send a mail based on form
* *
* Create the mail content and headers along to settings, form
* and fields datas; and update the form status (sent|error).
*/
public function sendMail()
{
$email = $name = $subject = $askcopy = null;
$tpl_data = (object) null;
$tpl_data->date = date('r');
$tpl_data->ip = $_SERVER["REMOTE_ADDR"];
$tpl_data->contact = $this->targets[0];
// fields
$tpl_data->fields = '';
foreach ($this->fields as $field) {
$tpl_data->fields .= $field->htmlMail();
switch ($field->type) {
case 'name':
$name = $field->value;
break;
case 'email':
$email = $field->value;
break;
case 'subject':
$subject = $field->value;
break;
case 'askcopy':
$askcopy = true;
break;
}
}
$html = $this->manager->renderTemplate('mail_template', $tpl_data);
$text = strip_tags($html);
if (empty($name)) {
$name = $this->lang('anonymous');
}
if (empty($subject)) {
$subject = $this->lang('nosubject');
}
// targets, subject, headers and multipart content
$targets = implode(',', $this->targets);
$encoded_subject = $this->encodeHeader($subject);
$mime_boundary = '----'.md5(time());
$headers = $this->mailHeaders($name, $email, $mime_boundary);
$content = $this->mailContent($text, 'plain', $mime_boundary);
$content .= $this->mailContent($html, 'html', $mime_boundary);
$content .= "--$mime_boundary--\n\n";
// debug
if ($this->config('debug')) {
$this->debug(array($headers, $targets, $subject, $text, $html));
return $this->setStatus('sent_debug');
}
// send mail
$success = mail($targets, $encoded_subject, $content, $headers);
// log
$this->manager->log(array(
date('d/m/Y H:i:s'), $targets, $subject, $name, $success ? 'success':'error'
));
if (!$success) {
return $this->setStatus('error');
}
if (!$email || !$askcopy) {
return $this->setStatus('sent');
}
// mail copy
$copy = mail($email, $encoded_subject, $content, $headers);
$this->setStatus($copy ? 'sent_copy' : 'sent_copy_error');
}
/**
* Return the mail headers
* @param string $name * @param string $name
* @param string $email * @param string $email
* @param string $mime_boundary * @param string $mime_boundary
*
* @return string * @return string
*/ */
private function mailHeaders($name, $email, $mime_boundary) private function mailHeaders($name, $email, $mime_boundary)
{ {
$encoded_name = $this->encodeHeader($name); $encoded_name = $this->encodeHeader($name);
$headers = "From: $encoded_name <no-reply@" . SERVERNAME . ">\n"; $headers = "From: {$encoded_name} <no-reply@".SERVERNAME.">\n";
if ($email) { if ($email) {
$headers .= "Reply-To: $encoded_name <$email>\n"; $headers .= "Reply-To: {$encoded_name} <{$email}>\n";
$headers .= "Return-Path: $encoded_name <$email>"; $headers .= "Return-Path: {$encoded_name} <{$email}>";
} }
$headers .= "\n"; $headers .= "\n";
$headers .= "MIME-Version: 1.0\n"; $headers .= "MIME-Version: 1.0\n";
$headers .= "Content-type: multipart/alternative; boundary=\"$mime_boundary\"\n"; $headers .= "Content-type: multipart/alternative; boundary=\"{$mime_boundary}\"\n";
$headers .= "X-Mailer: PHP/" . phpversion() . "\n"; $headers .= 'X-Mailer: PHP/'.phpversion()."\n";
return $headers; return $headers;
} }
/** /**
* Return a multipart/alternative content part. * Return a multipart/alternative content part.
*
* @param string $content * @param string $content
* @param string $type the content type (plain, html) * @param string $type the content type (plain, html)
* @param string $mime_boundary * @param string $mime_boundary
*
* @return string * @return string
*/ */
private function mailContent($content, $type, $mime_boundary) private function mailContent($content, $type, $mime_boundary)
{ {
$head = "--$mime_boundary\n"; $head = "--{$mime_boundary}\n";
$head .= "Content-Type: text/$type; charset=UTF-8\n"; $head .= "Content-Type: text/{$type}; charset=UTF-8\n";
$head .= "Content-Transfer-Encoding: 7bit\n\n"; $head .= "Content-Transfer-Encoding: 7bit\n\n";
return $head.$content."\n"; return $head.$content."\n";
} }
/** /**
* Format a string for UTF-8 email headers. * Format a string for UTF-8 email headers.
*
* @param string $string * @param string $string
*
* @return string * @return string
*/ */
private function encodeHeader($string) private function encodeHeader($string)
{ {
$string = base64_encode(html_entity_decode($string, ENT_COMPAT, 'UTF-8')); $string = base64_encode(html_entity_decode($string, ENT_COMPAT, 'UTF-8'));
return "=?UTF-8?B?$string?=";
}
/** return "=?UTF-8?B?{$string}?=";
* Return array of valid emails from a comma separated string
* @param string $emails
* @return array
*/
public static function getValidEmails($emails)
{
return array_filter(explode(',', $emails), function ($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL);
});
}
/**
* GETTERS / SETTERS
*/
/*
* Reset all fields values and errors
*/
public function reset()
{
foreach ($this->fields as $field) {
$field->value = '';
$field->error = '';
}
}
public function getTargets()
{
return $this->targets;
}
public function addTarget($tget)
{
if (in_array($tget, $this->targets) === false) {
$this->targets[] = $tget;
}
}
public function getField($id)
{
return $this->fields[$id];
}
public function getFields()
{
return $this->fields;
}
public function addField($field)
{
$this->fields[] = $field;
}
public function getStatus()
{
return $this->status;
}
public function setStatus($status)
{
if (!is_string($status)) {
return;
}
$this->status = $status;
if (substr($status, 0, 4) == 'sent') {
$this->sent = true;
}
}
public function getId()
{
return $this->id;
}
public function config($key)
{
return $this->manager->config($key);
}
public function lang($key)
{
return $this->manager->lang($key, $this->lang);
} }
} }