[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,10 +21,11 @@ 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
* @param int $id the field id * @param int $id the field id
* @param string $type the field type * @param string $type the field type
*/ */
public function __construct($form, $id, $type) public function __construct($form, $id, $type)
@ -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)
{ {
@ -131,10 +142,10 @@ class P01contactField
return false; return false;
} }
$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;
} }
@ -192,8 +204,8 @@ class P01contactField
*/ */
public function html() public function html()
{ {
$id = 'p01-contact' . $this->form->getId() . '_field' . $this->id; $id = 'p01-contact'.$this->form->getId().'_field'.$this->id;
$name = 'p01-contact_fields[' . $this->id . ']'; $name = 'p01-contact_fields['.$this->id.']';
$type = $this->getGeneralType(); $type = $this->getGeneralType();
$orig = $type != $this->type ? $this->type : ''; $orig = $type != $this->type ? $this->type : '';
$value = $this->value; $value = $this->value;
@ -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;">';
@ -281,7 +302,7 @@ class P01contactField
// properties // properties
$html .= '<td style="padding:.5em 1em; text-transform:lowercase; text-align:right; font-size:.875em; color:#888888; vertical-align: middle"><em>'; $html .= '<td style="padding:.5em 1em; text-transform:lowercase; text-align:right; font-size:.875em; color:#888888; vertical-align: middle"><em>';
if (!$this->value) { if (!$this->value) {
$html .= $this->form->lang('empty') . ' '; $html .= $this->form->lang('empty').' ';
} }
if ($this->title) { if ($this->title) {
$properties[] = $this->type; $properties[] = $this->type;
@ -289,21 +310,21 @@ 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);
} }
} }
if (count($properties)) { if (count($properties)) {
$html .= '(' . implode(', ', $properties) . ') '; $html .= '('.implode(', ', $properties).') ';
} }
$html .= '#' . $this->id; $html .= '#'.$this->id;
$html .= '</em></td></tr>'; $html .= '</em></td></tr>';
$html .= "\n\n"; $html .= "\n\n";
// value // value
if (!$this->value) { if (!$this->value) {
return $html . '</table>'; return $html.'</table>';
} }
$html .= '<tr><td colspan=2 style="padding:0">'; $html .= '<tr><td colspan=2 style="padding:0">';
$html .= '<div style="padding:.5em 1.5em;border:1px solid #ccc">'; $html .= '<div style="padding:.5em 1.5em;border:1px solid #ccc">';
@ -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,19 +371,20 @@ 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 {
$html .= ucfirst($this->form->lang($this->type)); $html .= ucfirst($this->form->lang($this->type));
} }
if ($this->description) { if ($this->description) {
$html .= ' <em class="description">' . $this->description . '</em>'; $html .= ' <em class="description">'.$this->description.'</em>';
} }
if ($this->error) { if ($this->error) {
$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,22 +393,23 @@ 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;
} }
} }
function preint($arr, $return = false) function preint($arr, $return = false)
{ {
$out = '<pre class="test" style="white-space:pre-wrap;">' . print_r(@$arr, true) . '</pre>'; $out = '<pre class="test" style="white-space:pre-wrap;">'.print_r(@$arr, true).'</pre>';
if ($return) { if ($return) {
return $out; return $out;
} }
@ -402,5 +428,6 @@ function unset_r($a, $i)
unset($a[$k][$i]); unset($a[$k][$i]);
} }
} }
return $a; return $a;
} }

View File

@ -1,41 +1,42 @@
<?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
* @param int $id the form id * @param int $id the form id
*/ */
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,67 +78,9 @@ class P01contactForm
$this->addTarget($email); $this->addTarget($email);
} }
} }
/**
* Create a field by parsing a tag parameter
*
* Find emails and parameters, create and setup form object.
* @param int $id the field id
* @param string $param the param to parse
*/
private function parseParam($id, $param)
{
$param_pattern = '`\s*([^ ,"=!]+)'; // type
$param_pattern.= '\s*(!)?'; // required!
$param_pattern.= '\s*(?:"([^"]*)")?'; // "title"
$param_pattern.= '\s*(?:\(([^"]*)\))?'; // (description)
$param_pattern.= '\s*(?:(=[><]?)?'; // =value, =>locked, =<placeholder
$param_pattern.= '\s*(.*))?\s*`'; // value
preg_match($param_pattern, $param, $param);
list(, $type, $required, $title, $desc, $assign, $values) = $param;
$field = new P01contactField($this, $id, $type);
// values
switch ($type) {
case 'select':
case 'radio':
case 'checkbox':
$field->value = explode('|', $values);
$field->resetSelectedValues();
break;
case 'askcopy':
// checkbox-like structure
$field->value = array($this->lang('askcopy'));
break;
case 'password':
// password value is required value
$field->required = $values;
break;
default:
if ($assign == '=<') {
$field->placeholder = $values;
} else {
// simple value
$field->value = $values;
}
}
// required
if ($type != 'password') {
$field->required = $required == '!';
}
if ($type == 'captcha') {
$field->required = true;
}
$field->title = $title;
$field->description = $desc;
$field->locked = $assign == '=>';
$this->addField($field);
}
/** /**
* Update POSTed form and try to send mail * Update POSTed form and try to send mail.
* *
* Check posted data, update form data, * Check posted data, update form data,
* define fields errors and form status. * define fields errors and form status.
@ -146,7 +89,7 @@ class P01contactForm
public function post() public function post()
{ {
if (empty($_POST['p01-contact_form']) if (empty($_POST['p01-contact_form'])
|| $_POST['p01-contact_form']['id'] != $this->id ) { || $_POST['p01-contact_form']['id'] != $this->id) {
return; return;
} }
@ -155,6 +98,7 @@ class P01contactForm
$this->setStatus('sent_already'); $this->setStatus('sent_already');
$this->setToken(); $this->setToken();
$this->reset(); $this->reset();
return; return;
} }
@ -175,13 +119,15 @@ class P01contactForm
// check errors and set status // check errors and set status
if ($this->config('disable')) { if ($this->config('disable')) {
$this->setStatus('disable'); $this->setStatus('disable');
return; return;
} }
if (count($this->targets) == 0) { if (0 == count($this->targets)) {
$this->setStatus('error_notarget'); $this->setStatus('error_notarget');
return; return;
} }
if ($hasFieldsErrors || $this->checkSpam($posted) !== true) { if ($hasFieldsErrors || true !== $this->checkSpam($posted)) {
return; return;
} }
@ -190,58 +136,9 @@ class P01contactForm
$this->reset(); $this->reset();
} }
/*
* SECURITY
*/
/** /**
* Check if the honeypot field is untouched and if the time between this post, * Get the token in Session (create it if not exists).
* the page load and previous posts and the hourly post count are valid
* according to the settings, and set the form status accordingly.
* *
* @param P01contact_form $form The submitted form
* @param array $post Sanitized p01-contact data of $_POST
* @return bool the result status
*/
private function checkSpam($post)
{
if (isset($post['totally_legit'])) {
$this->setStatus('error_honeypot');
return false;
}
$loads = Session::get('pageloads');
if (count($loads) > 1 && $loads[1] - $loads[0] < $this->config('min_sec_after_load')) {
$this->setStatus('error_pageload');
return false;
}
$lastpost = Session::get('lastpost', false);
if ($lastpost && time() - $lastpost < $this->config('min_sec_between_posts')) {
$this->setStatus('error_lastpost');
return false;
}
$postcount = Session::get('postcount', 0);
if (!$this->config('debug') && $postcount > $this->config('max_posts_by_hour')) {
$this->setStatus('error_postcount');
return false;
}
Session::set('lastpost', time());
Session::set('postcount', $postcount + 1);
return true;
}
/**
* Create an unique hash in Session
*/
private static function setToken()
{
Session::set('token', uniqid(md5(microtime()), true));
}
/**
* Get the token in Session (create it if not exists)
* @return string * @return string
*/ */
public function getToken() public function getToken()
@ -249,31 +146,22 @@ class P01contactForm
if (!Session::get('token', false)) { if (!Session::get('token', false)) {
$this->setToken(); $this->setToken();
} }
return Session::get('token'); return Session::get('token');
} }
/**
* Compare the POSTed token to the Session one
* @return boolean
*/
private function checkToken()
{
return $this->getToken() === $_POST['p01-contact_form']['token'];
}
/*
* RENDER
*/
// RENDER
/** /**
* Return the html display of the form * Return the html display of the form.
*
* @return string the <form> * @return string the <form>
*/ */
public function html() public function html()
{ {
$html = '<form action="'.PAGEURL.'#p01-contact'.$this->id.'" autocomplete="off" '; $html = '<div class="section">';
$html .= 'id="p01-contact' . $this->id . '" class="p01-contact" method="post">'; $html .= '<form action="'.PAGEURL.'#p01-contact'.$this->id.'" autocomplete="off" ';
$html .= 'id="p01-contact'.$this->id.'" class="p01-contact" method="post">';
if ($this->status) { if ($this->status) {
$html .= $this->htmlStatus(); $html .= $this->htmlStatus();
@ -285,30 +173,25 @@ class P01contactForm
if ($this->config('use_honeypot')) { 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 .= '<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 .= '<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 name="p01-contact_form[token]" type="hidden" value="'.$this->getToken().'" />';
$html .= '<input class="submit" type="submit" value="' . $this->lang('send') . '" /></div>'; $html .= '<input class="submit" type="submit" value="'.$this->lang('send').'" /></div>';
} }
$html .= '</form>'; $html .= '</form>';
$html .= '</div>';
if ($this->config('debug')) { if ($this->config('debug')) {
$html .= $this->debug(false); $html .= $this->debug(false);
} }
return $html; return $html;
} }
/**
* Return an html display of the form status
* @return string the <div>
*/
private function htmlStatus()
{
$statusclass = $this->sent ? 'alert success' : 'alert failed';
return '<div class="' . $statusclass . '">' . $this->lang($this->status) . '</div>';
}
/** /**
* Return P01contact_form infos. * Return P01contact_form infos.
*
* @param mixed $set_infos
*
* @return string * @return string
*/ */
public function debug($set_infos) public function debug($set_infos)
@ -317,31 +200,30 @@ class P01contactForm
static $post; static $post;
if ($set_infos) { if ($set_infos) {
$post = $set_infos; $post = $set_infos;
return; return;
} }
if ($post) { if ($post) {
list($headers, $targets, $subject, $text_content, $html_content) = $post; list($headers, $targets, $subject, $text_content, $html_content) = $post;
$out.= '<h3>Virtually sent mail :</h3>'; $out .= '<h3>Virtually sent mail :</h3>';
$out.= '<pre>'.htmlspecialchars($headers).'</pre>'; $out .= '<pre>'.htmlspecialchars($headers).'</pre>';
$out.= "<pre>Targets: $targets\nSubject: $subject</pre>"; $out .= "<pre>Targets: {$targets}\nSubject: {$subject}</pre>";
$out.= "Text content : <pre>$text_content</pre>"; $out .= "Text content : <pre>{$text_content}</pre>";
$out.= "HTML content : <div style=\"border:1px solid #ccc;\">$html_content</div>"; $out .= "HTML content : <div style=\"border:1px solid #ccc;\">{$html_content}</div>";
} }
$infos = $this; $infos = $this;
unset($infos->manager); unset($infos->manager);
$out .= "<h3>p01contact form $this->id :</h3>"; $out .= "<h3>p01contact form {$this->id} :</h3>";
$out .= preint($infos, true); $out .= preint($infos, true);
$out .= '</div>'; $out .= '</div>';
return $out; return $out;
} }
/* // MAIL
* MAIL
*/
/** /**
* Send a mail based on form * Send a mail based on form.
* *
* Create the mail content and headers along to settings, form * Create the mail content and headers along to settings, form
* and fields datas; and update the form status (sent|error). * and fields datas; and update the form status (sent|error).
@ -351,7 +233,7 @@ class P01contactForm
$email = $name = $subject = $askcopy = null; $email = $name = $subject = $askcopy = null;
$tpl_data = (object) null; $tpl_data = (object) null;
$tpl_data->date = date('r'); $tpl_data->date = date('r');
$tpl_data->ip = $_SERVER["REMOTE_ADDR"]; $tpl_data->ip = $_SERVER['REMOTE_ADDR'];
$tpl_data->contact = $this->targets[0]; $tpl_data->contact = $this->targets[0];
// fields // fields
$tpl_data->fields = ''; $tpl_data->fields = '';
@ -360,15 +242,19 @@ class P01contactForm
switch ($field->type) { switch ($field->type) {
case 'name': case 'name':
$name = $field->value; $name = $field->value;
break; break;
case 'email': case 'email':
$email = $field->value; $email = $field->value;
break; break;
case 'subject': case 'subject':
$subject = $field->value; $subject = $field->value;
break; break;
case 'askcopy': case 'askcopy':
$askcopy = true; $askcopy = true;
break; break;
} }
} }
@ -391,12 +277,12 @@ class P01contactForm
$content = $this->mailContent($text, 'plain', $mime_boundary); $content = $this->mailContent($text, 'plain', $mime_boundary);
$content .= $this->mailContent($html, 'html', $mime_boundary); $content .= $this->mailContent($html, 'html', $mime_boundary);
$content .= "--$mime_boundary--\n\n"; $content .= "--{$mime_boundary}--\n\n";
// debug // debug
if ($this->config('debug')) { if ($this->config('debug')) {
$this->debug(array($headers, $targets, $subject, $text, $html)); $this->debug([$headers, $targets, $subject, $text, $html]);
return $this->setStatus('sent_debug'); return $this->setStatus('sent_debug');
} }
@ -404,9 +290,9 @@ class P01contactForm
$success = mail($targets, $encoded_subject, $content, $headers); $success = mail($targets, $encoded_subject, $content, $headers);
// log // log
$this->manager->log(array( $this->manager->log([
date('d/m/Y H:i:s'), $targets, $subject, $name, $success ? 'success':'error' date('d/m/Y H:i:s'), $targets, $subject, $name, $success ? 'success' : 'error',
)); ]);
if (!$success) { if (!$success) {
return $this->setStatus('error'); return $this->setStatus('error');
@ -421,56 +307,10 @@ class P01contactForm
} }
/** /**
* Return the mail headers * Return array of valid emails from a comma separated string.
* @param string $name *
* @param string $email
* @param string $mime_boundary
* @return string
*/
private function mailHeaders($name, $email, $mime_boundary)
{
$encoded_name = $this->encodeHeader($name);
$headers = "From: $encoded_name <no-reply@" . SERVERNAME . ">\n";
if ($email) {
$headers .= "Reply-To: $encoded_name <$email>\n";
$headers .= "Return-Path: $encoded_name <$email>";
}
$headers .= "\n";
$headers .= "MIME-Version: 1.0\n";
$headers .= "Content-type: multipart/alternative; boundary=\"$mime_boundary\"\n";
$headers .= "X-Mailer: PHP/" . phpversion() . "\n";
return $headers;
}
/**
* Return a multipart/alternative content part.
* @param string $content
* @param string $type the content type (plain, html)
* @param string $mime_boundary
* @return string
*/
private function mailContent($content, $type, $mime_boundary)
{
$head = "--$mime_boundary\n";
$head .= "Content-Type: text/$type; charset=UTF-8\n";
$head .= "Content-Transfer-Encoding: 7bit\n\n";
return $head.$content."\n";
}
/**
* Format a string for UTF-8 email headers.
* @param string $string
* @return string
*/
private function encodeHeader($string)
{
$string = base64_encode(html_entity_decode($string, ENT_COMPAT, 'UTF-8'));
return "=?UTF-8?B?$string?=";
}
/**
* Return array of valid emails from a comma separated string
* @param string $emails * @param string $emails
*
* @return array * @return array
*/ */
public static function getValidEmails($emails) public static function getValidEmails($emails)
@ -481,12 +321,10 @@ class P01contactForm
} }
/** /**
* GETTERS / SETTERS * GETTERS / SETTERS.
*/ */
/* // Reset all fields values and errors
* Reset all fields values and errors
*/
public function reset() public function reset()
{ {
foreach ($this->fields as $field) { foreach ($this->fields as $field) {
@ -494,52 +332,256 @@ class P01contactForm
$field->error = ''; $field->error = '';
} }
} }
public function getTargets() public function getTargets()
{ {
return $this->targets; return $this->targets;
} }
public function addTarget($tget) public function addTarget($tget)
{ {
if (in_array($tget, $this->targets) === false) { if (false === in_array($tget, $this->targets)) {
$this->targets[] = $tget; $this->targets[] = $tget;
} }
} }
public function getField($id) public function getField($id)
{ {
return $this->fields[$id]; return $this->fields[$id];
} }
public function getFields() public function getFields()
{ {
return $this->fields; return $this->fields;
} }
public function addField($field) public function addField($field)
{ {
$this->fields[] = $field; $this->fields[] = $field;
} }
public function getStatus() public function getStatus()
{ {
return $this->status; return $this->status;
} }
public function setStatus($status) public function setStatus($status)
{ {
if (!is_string($status)) { if (!is_string($status)) {
return; return;
} }
$this->status = $status; $this->status = $status;
if (substr($status, 0, 4) == 'sent') { if ('sent' == substr($status, 0, 4)) {
$this->sent = true; $this->sent = true;
} }
} }
public function getId() public function getId()
{ {
return $this->id; return $this->id;
} }
public function config($key) public function config($key)
{ {
return $this->manager->config($key); return $this->manager->config($key);
} }
public function lang($key) public function lang($key)
{ {
return $this->manager->lang($key, $this->lang); return $this->manager->lang($key, $this->lang);
} }
/**
* Create a field by parsing a tag parameter.
*
* Find emails and parameters, create and setup form object.
*
* @param int $id the field id
* @param string $param the param to parse
*/
private function parseParam($id, $param)
{
$param_pattern = '`\s*([^ ,"=!]+)'; // type
$param_pattern .= '\s*(!)?'; // required!
$param_pattern .= '\s*(?:"([^"]*)")?'; // "title"
$param_pattern .= '\s*(?:\(([^"]*)\))?'; // (description)
$param_pattern .= '\s*(?:(=[><]?)?'; // =value, =>locked, =<placeholder
$param_pattern .= '\s*(.*))?\s*`'; // value
preg_match($param_pattern, $param, $param);
list(, $type, $required, $title, $desc, $assign, $values) = $param;
$field = new P01contactField($this, $id, $type);
// values
switch ($type) {
case 'select':
case 'radio':
case 'checkbox':
$field->value = explode('|', $values);
$field->resetSelectedValues();
break;
case 'askcopy':
// checkbox-like structure
$field->value = [$this->lang('askcopy')];
break;
case 'password':
// password value is required value
$field->required = $values;
break;
default:
if ('=<' == $assign) {
$field->placeholder = $values;
} else {
// simple value
$field->value = $values;
}
}
// required
if ('password' != $type) {
$field->required = '!' == $required;
}
if ('captcha' == $type) {
$field->required = true;
}
$field->title = $title;
$field->description = $desc;
$field->locked = '=>' == $assign;
$this->addField($field);
}
// SECURITY
/**
* Check if the honeypot field is untouched and if the time between this post,
* the page load and previous posts and the hourly post count are valid
* according to the settings, and set the form status accordingly.
*
* @param P01contact_form $form The submitted form
* @param array $post Sanitized p01-contact data of $_POST
*
* @return bool the result status
*/
private function checkSpam($post)
{
if (isset($post['totally_legit'])) {
$this->setStatus('error_honeypot');
return false;
}
$loads = Session::get('pageloads');
if (count($loads) > 1 && $loads[1] - $loads[0] < $this->config('min_sec_after_load')) {
$this->setStatus('error_pageload');
return false;
}
$lastpost = Session::get('lastpost', false);
if ($lastpost && time() - $lastpost < $this->config('min_sec_between_posts')) {
$this->setStatus('error_lastpost');
return false;
}
$postcount = Session::get('postcount', 0);
if (!$this->config('debug') && $postcount > $this->config('max_posts_by_hour')) {
$this->setStatus('error_postcount');
return false;
}
Session::set('lastpost', time());
Session::set('postcount', $postcount + 1);
return true;
}
/**
* Create an unique hash in Session.
*/
private static function setToken()
{
Session::set('token', uniqid(md5(microtime()), true));
}
/**
* Compare the POSTed token to the Session one.
*
* @return bool
*/
private function checkToken()
{
return $this->getToken() === $_POST['p01-contact_form']['token'];
}
/**
* Return an html display of the form status.
*
* @return string the <div>
*/
private function htmlStatus()
{
$statusclass = $this->sent ? 'alert success' : 'alert failed';
return '<div class="'.$statusclass.'">'.$this->lang($this->status).'</div>';
}
/**
* Return the mail headers.
*
* @param string $name
* @param string $email
* @param string $mime_boundary
*
* @return string
*/
private function mailHeaders($name, $email, $mime_boundary)
{
$encoded_name = $this->encodeHeader($name);
$headers = "From: {$encoded_name} <no-reply@".SERVERNAME.">\n";
if ($email) {
$headers .= "Reply-To: {$encoded_name} <{$email}>\n";
$headers .= "Return-Path: {$encoded_name} <{$email}>";
}
$headers .= "\n";
$headers .= "MIME-Version: 1.0\n";
$headers .= "Content-type: multipart/alternative; boundary=\"{$mime_boundary}\"\n";
$headers .= 'X-Mailer: PHP/'.phpversion()."\n";
return $headers;
}
/**
* Return a multipart/alternative content part.
*
* @param string $content
* @param string $type the content type (plain, html)
* @param string $mime_boundary
*
* @return string
*/
private function mailContent($content, $type, $mime_boundary)
{
$head = "--{$mime_boundary}\n";
$head .= "Content-Type: text/{$type}; charset=UTF-8\n";
$head .= "Content-Transfer-Encoding: 7bit\n\n";
return $head.$content."\n";
}
/**
* Format a string for UTF-8 email headers.
*
* @param string $string
*
* @return string
*/
private function encodeHeader($string)
{
$string = base64_encode(html_entity_decode($string, ENT_COMPAT, 'UTF-8'));
return "=?UTF-8?B?{$string}?=";
}
} }