546 lines
16 KiB
PHP
546 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* p01-contact - A simple contact forms manager
|
|
*
|
|
* @link https://github.com/nliautaud/p01contact
|
|
* @author Nicolas Liautaud
|
|
* @package p01contact
|
|
*/
|
|
namespace P01C;
|
|
|
|
require 'P01contact_Field.php';
|
|
|
|
class P01contactForm
|
|
{
|
|
private $manager;
|
|
|
|
private $id;
|
|
private $status;
|
|
private $targets;
|
|
private $fields;
|
|
public $lang;
|
|
public $sent;
|
|
|
|
/**
|
|
* @param P01contact $P01contact
|
|
* @param int $id the form id
|
|
*/
|
|
public function __construct($P01contact)
|
|
{
|
|
static $id;
|
|
$id++;
|
|
|
|
$this->manager = $P01contact;
|
|
|
|
$this->id = $id;
|
|
$this->status = '';
|
|
$this->targets = array();
|
|
$this->fields = array();
|
|
}
|
|
|
|
/**
|
|
* Find tag parameters, populate fields and targets.
|
|
*
|
|
* @param string $params the params
|
|
*/
|
|
public function parseTag($params)
|
|
{
|
|
// assure formating
|
|
$params = str_replace(' ', ' ', $params);
|
|
$params = strip_tags(str_replace("\n", '', $params));
|
|
$params = html_entity_decode($params, ENT_QUOTES, 'UTF-8');
|
|
|
|
// explode
|
|
$sep = $this->config('separator');
|
|
$params = array_filter(explode($sep, $params));
|
|
|
|
// emails
|
|
foreach ($params as $id => $param) {
|
|
$param = trim($param);
|
|
if (filter_var($param, FILTER_VALIDATE_EMAIL)) {
|
|
$this->addTarget($param);
|
|
unset($params[$id]);
|
|
}
|
|
}
|
|
// default params
|
|
if (empty($params)) {
|
|
$default = $this->config('default_params');
|
|
$params = array_filter(explode($sep, $default));
|
|
}
|
|
// create fields
|
|
foreach (array_values($params) as $id => $param) {
|
|
$this->parseParam($id, trim($param));
|
|
}
|
|
// default email addresses
|
|
$default_emails = $this->getValidEmails($this->config('default_email'));
|
|
foreach ($default_emails as $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
|
|
*
|
|
* 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,
|
|
* 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
|
|
*/
|
|
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()
|
|
{
|
|
return $this->getToken() === $_POST['p01-contact_form']['token'];
|
|
}
|
|
|
|
|
|
/*
|
|
* RENDER
|
|
*/
|
|
|
|
|
|
/**
|
|
* 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">';
|
|
|
|
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>
|
|
*/
|
|
private function htmlStatus()
|
|
{
|
|
$statusclass = $this->sent ? 'alert success' : 'alert failed';
|
|
return '<div class="' . $statusclass . '">' . $this->lang($this->status) . '</div>';
|
|
}
|
|
|
|
/**
|
|
* Return P01contact_form 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(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 $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
|
|
* @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);
|
|
}
|
|
}
|