[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
/**
* 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
* @package p01contact
*/
namespace P01C;
class P01contactField
{
private $form;
public $id;
public $type;
public $title;
@ -22,10 +21,11 @@ class P01contactField
public $required;
public $locked;
public $error;
private $form;
/**
* @param Form $form the container form
* @param int $id the field id
* @param Form $form the container form
* @param int $id the field id
* @param string $type the field 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
*/
@ -45,11 +45,12 @@ class P01contactField
// simple value
if (!is_array($this->value)) {
$this->value = htmlentities($new_value, ENT_COMPAT, 'UTF-8', false);
return;
}
// multiples-values (checkbox, radio, select)
if (!is_array($new_value)) {
$new_value = array($new_value);
$new_value = [$new_value];
}
foreach ($new_value as $i) {
$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()
{
$this->selected_values = array();
$this->selected_values = [];
foreach ($this->value as $i => $val) {
$value = preg_replace('`(^\s*:|:\s*$)`', '', $val, -1, $count);
if ($count) {
@ -73,32 +74,38 @@ class P01contactField
/**
* Check field value.
* @return boolean
*
* @return bool
*/
public function validate()
{
// empty and required
if (empty($this->value) && $this->required) {
$this->error = 'field_required';
return false;
}
// value blacklisted or not in whitelist
if ($reason = $this->isBlacklisted()) {
$this->error = 'field_' . $reason;
$this->error = 'field_'.$reason;
return false;
}
// not empty but not valid
if (!empty($this->value) && !$this->isValid()) {
$this->error = 'field_' . $this->type;
$this->error = 'field_'.$this->type;
return false;
}
return true;
}
/**
* Check if field value is valid
* Mean different things depending on field type
* @return boolean
* Mean different things depending on field type.
*
* @return bool
*/
public function isValid()
{
@ -107,6 +114,7 @@ class P01contactField
return filter_var($this->value, FILTER_VALIDATE_EMAIL);
case 'tel':
$pattern = '`^\+?[-0-9(). ]{6,}$$`i';
return preg_match($pattern, $this->value);
case 'url':
return filter_var($this->value, FILTER_VALIDATE_URL);
@ -122,8 +130,11 @@ class P01contactField
}
/**
* Check if reCaptcha is valid
* @return boolean
* Check if reCaptcha is valid.
*
* @param mixed $answer
*
* @return bool
*/
public function reCaptchaValidity($answer)
{
@ -131,10 +142,10 @@ class P01contactField
return false;
}
$params = [
'secret' => $this->form->config('recaptcha_secret_key'),
'response' => $answer
'secret' => $this->form->config('recaptcha_secret_key'),
'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')) {
$curl = curl_init($url);
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
* in value, and define if it should or should not be there.
*
* @return boolean
* @return bool
*/
public function isBlacklisted()
{
@ -173,14 +184,15 @@ class P01contactField
}
$content = array_filter(explode(',', $cl->content));
foreach ($content as $avoid) {
$found = preg_match("`$avoid`", $this->value);
$foundBlacklisted = $found && $cl->type == 'blacklist';
$notFoundWhitelisted = !$found && $cl->type == 'whitelist';
$found = preg_match("`{$avoid}`", $this->value);
$foundBlacklisted = $found && 'blacklist' == $cl->type;
$notFoundWhitelisted = !$found && 'whitelist' == $cl->type;
if ($foundBlacklisted || $notFoundWhitelisted) {
return $cl->type;
}
}
}
return false;
}
@ -192,8 +204,8 @@ class P01contactField
*/
public function html()
{
$id = 'p01-contact' . $this->form->getId() . '_field' . $this->id;
$name = 'p01-contact_fields[' . $this->id . ']';
$id = 'p01-contact'.$this->form->getId().'_field'.$this->id;
$name = 'p01-contact_fields['.$this->id.']';
$type = $this->getGeneralType();
$orig = $type != $this->type ? $this->type : '';
$value = $this->value;
@ -201,71 +213,80 @@ class P01contactField
$required = $this->required ? ' required ' : '';
$placeholder = $this->placeholder ? ' placeholder="'.$this->placeholder.'"' : '';
$is_single_option = is_array($this->value) && count($this->value) == 1;
if ($is_single_option) {
$html = "<div class=\"field inline $type $orig $required\">";
} else {
$html = "<div class=\"field $type $orig $required\">";
$is_single_option = is_array($this->value) && 1 == count($this->value) ? 'inline' : '';
$html = "<div class=\"row field {$is_single_option} {$type} {$orig} {$required}\">";
$html .= '<div class="col-sm-12 col-md-3">';
if ('' === $is_single_option) {
$html .= $this->htmlLabel($id);
}
$html .= '</div>';
$html .= '<div class="col-sm-12 col-md">';
switch ($type) {
case 'textarea':
$html .= '<textarea id="' . $id . '" rows="10" ';
$html .= 'name="' . $name . '"' . $disabled.$required.$placeholder;
$html .= '>' . $value . '</textarea>';
$html .= '<textarea id="'.$id.'" rows="10" ';
$html .= 'name="'.$name.'"'.$disabled.$required.$placeholder;
$html .= '>'.$value.'</textarea>';
break;
case 'captcha':
$key = $this->form->config('recaptcha_public_key');
if (!$key) {
break;
}
if ($this->form->getId() == 1) {
if (1 == $this->form->getId()) {
$html .= '<script src="https://www.google.com/recaptcha/api.js"></script>';
}
$html .='<div class="g-recaptcha" id="'.$id.'" data-sitekey="'.$key.'"></div>';
$html .="<input type=\"hidden\" id=\"$id\" name=\"$name\" value=\"trigger\">";
$html .= '<div class="g-recaptcha" id="'.$id.'" data-sitekey="'.$key.'"></div>';
$html .= "<input type=\"hidden\" id=\"{$id}\" name=\"{$name}\" value=\"trigger\">";
break;
case 'checkbox':
case 'radio':
$html .= '<div class="options">';
$html .= '<div class="options row">';
foreach ($this->value as $i => $v) {
$selected = $this->isSelected($i) ? ' checked' : '';
$v = !empty($v) ? $v : 'Default';
$html .= '<label class="option">';
$html .= '<label class="option col-sm-12">';
$html .= "<input id=\"{$id}_option{$i}\"";
$html .= " type=\"$type\" class=\"$type\" name=\"{$name}\"";
$html .= " value=\"$i\"$disabled$required$selected />$v";
$html .= " type=\"{$type}\" class=\"{$type}\" name=\"{$name}\"";
$html .= " value=\"{$i}\"{$disabled}{$required}{$selected} />{$v}";
$html .= '</label>';
}
$html .= '</div>';
break;
case 'select':
$html .= "<select id=\"$id\" name=\"$name\"$disabled$required>";
$html .= "<select id=\"{$id}\" name=\"{$name}\"{$disabled}{$required}>";
foreach ($this->value as $i => $v) {
$value = !empty($v) ? $v : 'Default';
$selected = $this->isSelected($i) ? ' selected="selected"' : '';
$html .= "<option id=\"{$id}_option{$i}\" value=\"$i\"$selected>";
$html .= $value . '</option>';
$html .= "<option id=\"{$id}_option{$i}\" value=\"{$i}\"{$selected}>";
$html .= $value.'</option>';
}
$html.= '</select>';
$html .= '</select>';
break;
default:
$html .= '<input id="' . $id . '" ';
$html .= 'name="' . $name . '" type="'.$type.'" ';
$html .= 'value="' . $value . '"' . $disabled.$required.$placeholder . ' />';
$html .= '<input id="'.$id.'" ';
$html .= 'name="'.$name.'" type="'.$type.'" ';
$html .= 'value="'.$value.'"'.$disabled.$required.$placeholder.' />';
break;
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
/*
* Return a html presentation of the field value.
*/
// Return a html presentation of the field value.
public function htmlMail()
{
$gen_type = $this->getGeneralType();
$properties = array();
$properties = [];
$html = '<table style="width: 100%; margin: 1em 0; border-collapse: collapse;">';
@ -281,7 +302,7 @@ class P01contactField
// properties
$html .= '<td style="padding:.5em 1em; text-transform:lowercase; text-align:right; font-size:.875em; color:#888888; vertical-align: middle"><em>';
if (!$this->value) {
$html .= $this->form->lang('empty') . ' ';
$html .= $this->form->lang('empty').' ';
}
if ($this->title) {
$properties[] = $this->type;
@ -289,21 +310,21 @@ class P01contactField
if ($gen_type != $this->type) {
$properties[] = $gen_type;
}
foreach (array('locked', 'required') as $property) {
if ($this->$property) {
foreach (['locked', 'required'] as $property) {
if ($this->{$property}) {
$properties[] = $this->form->lang($property);
}
}
if (count($properties)) {
$html .= '(' . implode(', ', $properties) . ') ';
$html .= '('.implode(', ', $properties).') ';
}
$html .= '#' . $this->id;
$html .= '#'.$this->id;
$html .= '</em></td></tr>';
$html .= "\n\n";
// value
if (!$this->value) {
return $html . '</table>';
return $html.'</table>';
}
$html .= '<tr><td colspan=2 style="padding:0">';
$html .= '<div style="padding:.5em 1.5em;border:1px solid #ccc">';
@ -323,15 +344,18 @@ class P01contactField
$html .= empty($v) ? 'Default' : $v;
$html .= "</div>\n";
}
break;
default:
$address = '~[[:alpha:]]+://[^<>[:space:]]+[[:alnum:]/]~';
$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;
}
$html .= '</div></td></tr></table>';
return $html;
}
@ -347,19 +371,20 @@ class P01contactField
*/
private function htmlLabel($for)
{
$html = '<label for="' . $for . '">';
$html .= '<label for="'.$for.'" class="doc">';
if ($this->title) {
$html .= $this->title;
} else {
$html .= ucfirst($this->form->lang($this->type));
}
if ($this->description) {
$html .= ' <em class="description">' . $this->description . '</em>';
$html .= ' <em class="description">'.$this->description.'</em>';
}
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>';
return $html;
}
@ -368,22 +393,23 @@ class P01contactField
*/
private function getGeneralType()
{
$types = array(
'name' => 'text',
$types = [
'name' => 'text',
'subject' => 'text',
'message' => 'textarea',
'askcopy' => 'checkbox'
);
'askcopy' => 'checkbox',
];
if (isset($types[$this->type])) {
return $types[$this->type];
}
return $this->type;
}
}
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) {
return $out;
}
@ -402,5 +428,6 @@ function unset_r($a, $i)
unset($a[$k][$i]);
}
}
return $a;
}

View File

@ -1,41 +1,42 @@
<?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
* @package p01contact
*/
namespace P01C;
require 'P01contact_Field.php';
class P01contactForm
{
public $lang;
public $sent;
private $manager;
private $id;
private $status;
private $targets;
private $fields;
public $lang;
public $sent;
/**
* @param P01contact $P01contact
* @param int $id the form id
* @param int $id the form id
*/
public function __construct($P01contact)
{
static $id;
$id++;
++$id;
$this->manager = $P01contact;
$this->id = $id;
$this->status = '';
$this->targets = array();
$this->fields = array();
$this->targets = [];
$this->fields = [];
}
/**
@ -77,67 +78,9 @@ class P01contactForm
$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,
* define fields errors and form status.
@ -146,7 +89,7 @@ class P01contactForm
public function post()
{
if (empty($_POST['p01-contact_form'])
|| $_POST['p01-contact_form']['id'] != $this->id ) {
|| $_POST['p01-contact_form']['id'] != $this->id) {
return;
}
@ -155,6 +98,7 @@ class P01contactForm
$this->setStatus('sent_already');
$this->setToken();
$this->reset();
return;
}
@ -175,13 +119,15 @@ class P01contactForm
// check errors and set status
if ($this->config('disable')) {
$this->setStatus('disable');
return;
}
if (count($this->targets) == 0) {
if (0 == count($this->targets)) {
$this->setStatus('error_notarget');
return;
}
if ($hasFieldsErrors || $this->checkSpam($posted) !== true) {
if ($hasFieldsErrors || true !== $this->checkSpam($posted)) {
return;
}
@ -190,58 +136,9 @@ class P01contactForm
$this->reset();
}
/*
* 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.
* Get the token in Session (create it if not exists).
*
* @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
*/
public function getToken()
@ -249,31 +146,22 @@ class P01contactForm
if (!Session::get('token', false)) {
$this->setToken();
}
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>
*/
public function html()
{
$html = '<form action="'.PAGEURL.'#p01-contact'.$this->id.'" autocomplete="off" ';
$html .= 'id="p01-contact' . $this->id . '" class="p01-contact" method="post">';
$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();
@ -285,30 +173,25 @@ class P01contactForm
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 .= '<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 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.
*
* @param mixed $set_infos
*
* @return string
*/
public function debug($set_infos)
@ -317,31 +200,30 @@ class P01contactForm
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>";
$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 .= "<h3>p01contact form {$this->id} :</h3>";
$out .= preint($infos, true);
$out .= '</div>';
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
* and fields datas; and update the form status (sent|error).
@ -351,7 +233,7 @@ class P01contactForm
$email = $name = $subject = $askcopy = null;
$tpl_data = (object) null;
$tpl_data->date = date('r');
$tpl_data->ip = $_SERVER["REMOTE_ADDR"];
$tpl_data->ip = $_SERVER['REMOTE_ADDR'];
$tpl_data->contact = $this->targets[0];
// fields
$tpl_data->fields = '';
@ -360,15 +242,19 @@ class P01contactForm
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;
}
}
@ -391,12 +277,12 @@ class P01contactForm
$content = $this->mailContent($text, 'plain', $mime_boundary);
$content .= $this->mailContent($html, 'html', $mime_boundary);
$content .= "--$mime_boundary--\n\n";
$content .= "--{$mime_boundary}--\n\n";
// 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');
}
@ -404,9 +290,9 @@ class P01contactForm
$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'
));
$this->manager->log([
date('d/m/Y H:i:s'), $targets, $subject, $name, $success ? 'success' : 'error',
]);
if (!$success) {
return $this->setStatus('error');
@ -421,56 +307,10 @@ class P01contactForm
}
/**
* 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?=";
}
/**
* Return array of valid emails from a comma separated string
* Return array of valid emails from a comma separated string.
*
* @param string $emails
*
* @return array
*/
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()
{
foreach ($this->fields as $field) {
@ -494,52 +332,256 @@ class P01contactForm
$field->error = '';
}
}
public function getTargets()
{
return $this->targets;
}
public function addTarget($tget)
{
if (in_array($tget, $this->targets) === false) {
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 (substr($status, 0, 4) == 'sent') {
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.
*
* @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}?=";
}
}