[TASK] update and formatting
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/**
|
||||
* p01-contact - A simple contact forms manager
|
||||
*
|
||||
* @package p01contact
|
||||
* @link https://github.com/nliautaud/p01contact
|
||||
* @author Nicolas Liautaud
|
||||
*/
|
||||
namespace P01C;
|
||||
|
||||
require_once 'P01contact_Session.php';
|
||||
require_once 'P01contact_Form.php';
|
||||
require_once 'vendor/spyc.php';
|
||||
|
||||
class P01contact
|
||||
{
|
||||
public $version;
|
||||
public $default_lang;
|
||||
private $config;
|
||||
private $langs;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
define('P01C\VERSION', '1.1.6');
|
||||
$this->version = VERSION;
|
||||
|
||||
define('P01C\SERVERNAME', $_SERVER['SERVER_NAME']);
|
||||
define('P01C\SERVERPORT', $_SERVER['SERVER_PORT']);
|
||||
define('P01C\SCRIPTNAME', $_SERVER['SCRIPT_NAME']);
|
||||
define('P01C\SCRIPTPATH', get_included_files()[0]);
|
||||
|
||||
define('P01C\HTTPS', !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
define('P01C\PORT', SERVERPORT && SERVERPORT != 80 && SERVERPORT != 443 ? ':'.SERVERPORT : '');
|
||||
define('P01C\PROTOCOL', HTTPS || SERVERPORT == 443 ? 'https' : 'http');
|
||||
define('P01C\SERVER', PROTOCOL . '://' . SERVERNAME . PORT);
|
||||
define('P01C\PAGEURI', $_SERVER['REQUEST_URI']);
|
||||
define('P01C\PAGEURL', SERVER . PAGEURI);
|
||||
|
||||
define('P01C\PATH', dirname(__DIR__) . '/');
|
||||
define('P01C\ROOT', str_replace(SCRIPTNAME,'', SCRIPTPATH));
|
||||
define('P01C\RELPATH', str_replace(ROOT, '', PATH));
|
||||
|
||||
define('P01C\LANGSPATH', PATH . 'lang/');
|
||||
define('P01C\TPLPATH', PATH . 'src/templates/');
|
||||
define('P01C\CONFIGPATH', PATH . 'config.json');
|
||||
define('P01C\LOGPATH', PATH . 'log.json');
|
||||
|
||||
define('P01C\REPOURL', 'https://github.com/nliautaud/p01contact');
|
||||
define('P01C\WIKIURL', 'https://github.com/nliautaud/p01contact/wiki');
|
||||
define('P01C\ISSUESURL', 'https://github.com/nliautaud/p01contact/issues');
|
||||
define('P01C\APILATEST', 'https://api.github.com/repos/nliautaud/p01contact/releases/latest');
|
||||
|
||||
$this->loadConfig();
|
||||
$this->loadLangs();
|
||||
|
||||
if ($this->config('debug')) {
|
||||
$this->enablePHPdebug();
|
||||
}
|
||||
|
||||
Session::stack('pageloads', time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the releases API and return the new release infos, if there is one.
|
||||
*
|
||||
* @see https://developer.github.com/v3/repos/releases/#get-the-latest-release
|
||||
* @return object the release infos
|
||||
*/
|
||||
public function getNewRelease()
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, APILATEST);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'p01contact/curl');
|
||||
$resp = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if ($resp) {
|
||||
return json_decode($resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string to replace tags by forms
|
||||
*
|
||||
* Find tags, create forms structures, check POST and modify string.
|
||||
* @param string $contents the string to parse
|
||||
* @return string the modified string
|
||||
*/
|
||||
public function parse($contents)
|
||||
{
|
||||
$sp = '(?:\s|</?p>)*';
|
||||
$pattern = "`(?<!<code>)\(%\s*contact\s*(\w*)\s*:?$sp(.*?)$sp%\)`s";
|
||||
preg_match_all($pattern, $contents, $tags, PREG_SET_ORDER);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$form = $this->newForm($tag[2], $tag[1]);
|
||||
$contents = preg_replace($pattern, $form, $contents, 1);
|
||||
}
|
||||
return $contents;
|
||||
}
|
||||
/**
|
||||
* Return a form based on the given parameters and lang
|
||||
*
|
||||
* @param string $params the parameters string, according to the syntax
|
||||
* @param string $lang form-specific language code
|
||||
* @return string the html form
|
||||
*/
|
||||
public function newForm($params = '', $lang = null)
|
||||
{
|
||||
$defaultStyle = '';
|
||||
static $once;
|
||||
if (!$once) {
|
||||
$defaultStyle = '<link rel="stylesheet" href="'.SERVER.RELPATH.'style.css"/>';
|
||||
$once = true;
|
||||
}
|
||||
$form = new P01contactForm($this);
|
||||
$form->parseTag($params);
|
||||
if ($lang) {
|
||||
$form->lang = $lang;
|
||||
}
|
||||
$form->post();
|
||||
|
||||
return $defaultStyle . $form->html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display system and P01contact infos.
|
||||
*
|
||||
* @return string the html report
|
||||
*/
|
||||
public function debugReport()
|
||||
{
|
||||
$out = '<h2 style="color:#c33">p01-contact debug</h2>';
|
||||
|
||||
$out.= '<h3>Health :</h3>';
|
||||
$health = 'PHP version : '.phpversion()."\n";
|
||||
$health.= 'PHP mbstring (UTF-8) : '.(extension_loaded('mbstring') ? 'OK' : 'MISSING');
|
||||
$out.= preint($health, true);
|
||||
|
||||
$out.= '<h3>Constants :</h3>';
|
||||
$constants = get_defined_constants(true)['user'];
|
||||
$filteredConstants = array_filter(array_keys($constants), function ($key) {
|
||||
return 0 === strpos($key, __namespace__);
|
||||
});
|
||||
$filteredConstants = array_intersect_key($constants, array_flip($filteredConstants));
|
||||
$out .= preint($filteredConstants, true);
|
||||
|
||||
$out .= Session::report();
|
||||
|
||||
if (!empty($_POST)) {
|
||||
$out.= '<h3>$_POST :</h3>';
|
||||
$out.= preint($_POST, true);
|
||||
}
|
||||
$out.= '<h3>$p01contact :</h3>';
|
||||
$out.= preint($this, true);
|
||||
return $out;
|
||||
}
|
||||
/**
|
||||
* Enable PHP error reporting
|
||||
*/
|
||||
public function enablePHPdebug()
|
||||
{
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* LANG
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Load language files
|
||||
*/
|
||||
private function loadLangs()
|
||||
{
|
||||
$this->langs = [];
|
||||
$files = glob(LANGSPATH . '*.yml');
|
||||
foreach ($files as $f) {
|
||||
$parsed = \Spyc::YAMLLoad($f);
|
||||
if (!$parsed || !isset($parsed['key'])) {
|
||||
continue;
|
||||
}
|
||||
$this->langs[$parsed['key']] = $parsed;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return a traduction of the keyword
|
||||
*
|
||||
* Manage languages between requested langs and existing traductions.
|
||||
* @param string $key the keyword
|
||||
* @return string
|
||||
*/
|
||||
public function lang($key, $lang = null)
|
||||
{
|
||||
$default = !empty($this->default_lang) ? $this->default_lang : 'en';
|
||||
|
||||
if (!$lang) {
|
||||
$lang = $this->config('lang');
|
||||
}
|
||||
|
||||
if (empty($lang)
|
||||
|| !isset($this->langs[$lang])
|
||||
|| !isset($this->langs[$lang]['strings'][$key])) {
|
||||
$lang = $default;
|
||||
}
|
||||
|
||||
$strings = $this->langs[$lang]['strings'];
|
||||
if (!empty($strings[$key])) {
|
||||
return trim($strings[$key]);
|
||||
}
|
||||
return ucfirst($key);
|
||||
}
|
||||
/**
|
||||
* Return the languages objects
|
||||
* @return array
|
||||
*/
|
||||
public function langs()
|
||||
{
|
||||
return $this->langs;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* CONFIG
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Load the JSON configuration file.
|
||||
*/
|
||||
private function loadConfig()
|
||||
{
|
||||
$content = file_exists(CONFIGPATH) ? file_get_contents(CONFIGPATH) : null;
|
||||
$this->config = $content ? json_decode($content) : (object) array();
|
||||
$this->setDefaultConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the obligatory settings if missing.
|
||||
*/
|
||||
private function setDefaultConfig()
|
||||
{
|
||||
$default = array(
|
||||
'default_params' => 'name!, email!, subject!, message!',
|
||||
'separator' => ',',
|
||||
'logs_count' => 10,
|
||||
'use_honeypot' => true,
|
||||
'min_sec_after_load' => '3',
|
||||
'max_posts_by_hour' => '10',
|
||||
'min_sec_between_posts' => '5',
|
||||
);
|
||||
foreach ($default as $key => $value) {
|
||||
if (empty($this->config->{$key})) {
|
||||
$this->config->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the logs.
|
||||
*/
|
||||
public function log($data)
|
||||
{
|
||||
if (!$this->config('logs_count')) {
|
||||
return;
|
||||
}
|
||||
$logs = json_decode(@file_get_contents(LOGPATH));
|
||||
$logs[] = $data;
|
||||
$max = max(0, intval($this->config('logs_count')));
|
||||
|
||||
while (count($logs) > $max) {
|
||||
array_shift($logs);
|
||||
}
|
||||
$this->updateJSON(LOGPATH, $logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a JSON file with new data.
|
||||
*
|
||||
* @param string $file_path the config file path
|
||||
* @param array $new_values the new values to write
|
||||
* @param array $old_values the values to change
|
||||
* @return boolean file edition sucess
|
||||
*/
|
||||
private function updateJSON($path, $new_values)
|
||||
{
|
||||
if ($file = fopen($path, 'w')) {
|
||||
fwrite($file, json_encode($new_values, JSON_PRETTY_PRINT));
|
||||
fclose($file);
|
||||
return true;
|
||||
} return false;
|
||||
}
|
||||
/**
|
||||
* Return a setting value from the config.
|
||||
* @param mixed $key the setting key, or an array as path to sub-key
|
||||
* @return mixed the setting value
|
||||
*/
|
||||
public function config($key)
|
||||
{
|
||||
if (!is_array($key)) {
|
||||
$key = array($key);
|
||||
}
|
||||
$curr = $this->config;
|
||||
foreach ($key as $k) {
|
||||
if (is_numeric($k)) {
|
||||
$k = intval($k);
|
||||
if (!isset($curr[$k])) {
|
||||
return;
|
||||
}
|
||||
$curr = $curr[$k];
|
||||
} else {
|
||||
if (!isset($curr->$k)) {
|
||||
return;
|
||||
}
|
||||
$curr = $curr->$k;
|
||||
}
|
||||
$k = $curr;
|
||||
}
|
||||
return $k;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* TEMPLATES
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Return a template file content
|
||||
*/
|
||||
public function getTemplate($name)
|
||||
{
|
||||
static $cache;
|
||||
if (isset($cache[$name])) {
|
||||
return $cache[$name];
|
||||
}
|
||||
if (!isset($cache)) {
|
||||
$cache = array();
|
||||
}
|
||||
$cache[$name] = @file_get_contents(TPLPATH . $name . '.html');
|
||||
return $cache[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the obligatory settings if missing.
|
||||
*/
|
||||
public function renderTemplate($name, $data)
|
||||
{
|
||||
$html = $this->getTemplate($name);
|
||||
// config
|
||||
$html = preg_replace_callback('`config\((.+)\)`', function ($matches) {
|
||||
return $this->config(explode(',', $matches[1]));
|
||||
}, $html);
|
||||
// lang
|
||||
$html = preg_replace_callback('`{{lang\.(\w+)}}`', function ($matches) {
|
||||
return $this->lang($matches[1]);
|
||||
}, $html);
|
||||
// constants
|
||||
$html = preg_replace_callback('`{{([A-Z]{3,})}}`', function ($matches) {
|
||||
return constant(__namespace__.'\\'.$matches[1]);
|
||||
}, $html);
|
||||
// data
|
||||
$html = preg_replace_callback('`{{(\w+)}}`', function ($matches) use ($data) {
|
||||
return @$data->{$matches[1]};
|
||||
}, $html);
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* PANEL
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Save settings if necessary and display configuration panel content
|
||||
* Parse and replace values in php config file by POST values.
|
||||
*/
|
||||
public function panel()
|
||||
{
|
||||
if (isset($_POST['p01-contact']['settings'])) {
|
||||
$success = $this->updateJSON(CONFIGPATH, $_POST['p01-contact']['settings']);
|
||||
$this->loadConfig();
|
||||
|
||||
if ($success) {
|
||||
$msg = '<div class="updated">' . $this->lang('config_updated') . '</div>';
|
||||
} else {
|
||||
$msg = '<div class="error">'.$this->lang('config_error_modify');
|
||||
$msg.= '<pre>'.CONFIGPATH.'</pre></div>';
|
||||
}
|
||||
return $msg . $this->panelContent();
|
||||
}
|
||||
return $this->panelContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return configuration panel content, replacing the following in the template :
|
||||
*
|
||||
* - lang(key) : language string
|
||||
* - config(key,...) : value of a config setting
|
||||
* - other(key) : other value pre-defined
|
||||
* - VALUE : constant value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function panelContent($system = 'gs')
|
||||
{
|
||||
$debug = $this->config('debug');
|
||||
$tpl_data = (object) null;
|
||||
$tpl_data->disablechecked = $this->config('disable') ? 'checked="checked" ' : '';
|
||||
$tpl_data->debugchecked = $debug ? 'checked="checked" ' : '';
|
||||
$tpl_data->honeypotchecked = $this->config('use_honeypot') ? 'checked="checked" ' : '';
|
||||
$tpl_data->default_lang = $this->default_lang;
|
||||
$tpl_data->version = $this->version;
|
||||
|
||||
$list = $this->config('checklist');
|
||||
if ($list) {
|
||||
foreach ($list as $i => $cl) {
|
||||
$bl = 'cl'.$i.'bl';
|
||||
$wl = 'cl'.$i.'wl';
|
||||
$tpl_data->$bl = isset($cl->type) && $cl->type == 'whitelist' ? '' : 'checked';
|
||||
$tpl_data->$wl = $tpl_data->$bl ? '' : 'checked';
|
||||
}
|
||||
}
|
||||
|
||||
$lang = $this->config('lang');
|
||||
$tpl_data->langsoptions = '<option value=""'.($lang==''?' selected="selected" ':'').'>Default</option>';
|
||||
foreach ($this->langs() as $language) {
|
||||
$tpl_data->langsoptions .= '<option value="' . $language['key'] . '" ';
|
||||
if ($lang == $language['key']) {
|
||||
$tpl_data->langsoptions .= 'selected="selected" ';
|
||||
}
|
||||
$tpl_data->langsoptions .= '>' . $language['english_name'] . '</option>';
|
||||
}
|
||||
|
||||
$html = $this->renderTemplate($system.'_settings', $tpl_data);
|
||||
|
||||
//new release
|
||||
$infos = '';
|
||||
if ($response = $this->getNewRelease()) {
|
||||
if ($debug && isset($response->message)) {
|
||||
$infos .= '<div class="updated">New release check error debug : Github ';
|
||||
$infos .= $response->message . '</div>';
|
||||
}
|
||||
if (isset($response->url) && version_compare($response->tag_name, $this->version) > 0) {
|
||||
$infos .= '<div class="updated">' . $this->lang('new_release');
|
||||
$infos .= '<br /><a href="' . $response->html_url . '">';
|
||||
$infos .= $this->lang('download') . ' (' . $response->tag_name . ')</a></div>';
|
||||
}
|
||||
}
|
||||
|
||||
$logsblock = $this->logsTable();
|
||||
|
||||
return $infos . $html . $logsblock;
|
||||
}
|
||||
|
||||
private function logsTable()
|
||||
{
|
||||
$logs = json_decode(@file_get_contents(LOGPATH));
|
||||
if (!$logs) {
|
||||
return;
|
||||
}
|
||||
$html = '';
|
||||
foreach (array_reverse($logs) as $log) {
|
||||
$html .= '<tr><td>';
|
||||
$html .= implode('</td><td>', array_map('htmlentities', $log));
|
||||
$html .= '</td></tr>';
|
||||
}
|
||||
return '<div class="logs"><h2>Logs</h2><table>'.$html.'</table></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
/**
|
||||
* p01-contact - A simple contact forms manager
|
||||
*
|
||||
* @link https://github.com/nliautaud/p01contact
|
||||
* @author Nicolas Liautaud
|
||||
* @package p01contact
|
||||
*/
|
||||
namespace P01C;
|
||||
|
||||
class P01contactField
|
||||
{
|
||||
private $form;
|
||||
|
||||
public $id;
|
||||
public $type;
|
||||
public $title;
|
||||
public $description;
|
||||
public $value;
|
||||
public $selected_values;
|
||||
public $placeholder;
|
||||
public $required;
|
||||
public $locked;
|
||||
public $error;
|
||||
|
||||
/**
|
||||
* @param Form $form the container form
|
||||
* @param int $id the field id
|
||||
* @param string $type the field type
|
||||
*/
|
||||
public function __construct($form, $id, $type)
|
||||
{
|
||||
$this->form = $form;
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the field value or selected value
|
||||
*
|
||||
* @param mixed $new_value the value, or an array of selected values ids
|
||||
*/
|
||||
public function setValue($new_value)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
foreach ($new_value as $i) {
|
||||
$this->selected_values[intval($i)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the selected values by finding ones who starts or end with ":"
|
||||
*/
|
||||
public function resetSelectedValues()
|
||||
{
|
||||
$this->selected_values = array();
|
||||
foreach ($this->value as $i => $val) {
|
||||
$value = preg_replace('`(^\s*:|:\s*$)`', '', $val, -1, $count);
|
||||
if ($count) {
|
||||
$this->value[$i] = $value;
|
||||
$this->selected_values[$i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check field value.
|
||||
* @return boolean
|
||||
*/
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
// not empty but not valid
|
||||
if (!empty($this->value) && !$this->isValid()) {
|
||||
$this->error = 'field_' . $this->type;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field value is valid
|
||||
* Mean different things depending on field type
|
||||
* @return boolean
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
switch ($this->type) {
|
||||
case 'email':
|
||||
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);
|
||||
case 'message':
|
||||
return strlen($this->value) > $this->form->config('message_len');
|
||||
case 'captcha':
|
||||
return $this->reCaptchaValidity($_POST['g-recaptcha-response']);
|
||||
case 'password':
|
||||
return $this->value == $this->required;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if reCaptcha is valid
|
||||
* @return boolean
|
||||
*/
|
||||
public function reCaptchaValidity($answer)
|
||||
{
|
||||
if (!$answer) {
|
||||
return false;
|
||||
}
|
||||
$params = [
|
||||
'secret' => $this->form->config('recaptcha_secret_key'),
|
||||
'response' => $answer
|
||||
];
|
||||
$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);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 1);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$response = curl_exec($curl);
|
||||
} else {
|
||||
$response = file_get_contents($url);
|
||||
}
|
||||
|
||||
if (empty($response) || is_null($response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return json_decode($response)->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function isBlacklisted()
|
||||
{
|
||||
$list = $this->form->config('checklist');
|
||||
if (!$list) {
|
||||
return;
|
||||
}
|
||||
foreach ($list as $cl) {
|
||||
if ($cl->name != $this->type) {
|
||||
continue;
|
||||
}
|
||||
$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';
|
||||
if ($foundBlacklisted || $notFoundWhitelisted) {
|
||||
return $cl->type;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the html display of the field
|
||||
*
|
||||
* Manage field title, error message, and type-based display
|
||||
* @return string the <div>
|
||||
*/
|
||||
public function html()
|
||||
{
|
||||
$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;
|
||||
$disabled = $this->locked ? ' disabled="disabled"' : '';
|
||||
$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\">";
|
||||
$html .= $this->htmlLabel($id);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case '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) {
|
||||
$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\">";
|
||||
break;
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
$html .= '<div class="options">';
|
||||
foreach ($this->value as $i => $v) {
|
||||
$selected = $this->isSelected($i) ? ' checked' : '';
|
||||
$v = !empty($v) ? $v : 'Default';
|
||||
$html .= '<label class="option">';
|
||||
$html .= "<input id=\"{$id}_option{$i}\"";
|
||||
$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>";
|
||||
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.= '</select>';
|
||||
break;
|
||||
default:
|
||||
$html .= '<input id="' . $id . '" ';
|
||||
$html .= 'name="' . $name . '" type="'.$type.'" ';
|
||||
$html .= 'value="' . $value . '"' . $disabled.$required.$placeholder . ' />';
|
||||
break;
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
/*
|
||||
* Return a html presentation of the field value.
|
||||
*/
|
||||
public function htmlMail()
|
||||
{
|
||||
$gen_type = $this->getGeneralType();
|
||||
$properties = array();
|
||||
|
||||
$html = '<table style="width: 100%; margin: 1em 0; border-collapse: collapse;">';
|
||||
|
||||
// name
|
||||
$emphasis = $this->value ? 'font-weight:bold' : 'font-style:italic';
|
||||
$html .= "\n\n\n";
|
||||
$html .= '<tr style="background-color: #eeeeee">';
|
||||
$html .= '<td style="padding: .5em .75em"><span style="'.$emphasis.'">';
|
||||
$html .= $this->title ? $this->title : ucfirst($this->form->lang($this->type));
|
||||
$html .= '</span></td>';
|
||||
$html .= "\t\t";
|
||||
|
||||
// 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') . ' ';
|
||||
}
|
||||
if ($this->title) {
|
||||
$properties[] = $this->type;
|
||||
}
|
||||
if ($gen_type != $this->type) {
|
||||
$properties[] = $gen_type;
|
||||
}
|
||||
foreach (array('locked', 'required') as $property) {
|
||||
if ($this->$property) {
|
||||
$properties[] = $this->form->lang($property);
|
||||
}
|
||||
}
|
||||
if (count($properties)) {
|
||||
$html .= '(' . implode(', ', $properties) . ') ';
|
||||
}
|
||||
$html .= '#' . $this->id;
|
||||
$html .= '</em></td></tr>';
|
||||
$html .= "\n\n";
|
||||
|
||||
// value
|
||||
if (!$this->value) {
|
||||
return $html . '</table>';
|
||||
}
|
||||
$html .= '<tr><td colspan=2 style="padding:0">';
|
||||
$html .= '<div style="padding:.5em 1.5em;border:1px solid #ccc">';
|
||||
switch ($gen_type) {
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
case 'select':
|
||||
foreach ($this->value as $i => $v) {
|
||||
if ($this->isSelected($i)) {
|
||||
$html .= '<div>';
|
||||
$checkmark = '☑';
|
||||
} else {
|
||||
$html .= '<div style="color:#ccc; font-style:italic">';
|
||||
$checkmark = '☐';
|
||||
}
|
||||
$html .= '<span style="font-size:1.5em; vertical-align:middle; margin-right:.5em; font-style:normal">'.$checkmark.'</span>';
|
||||
$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>";
|
||||
break;
|
||||
}
|
||||
|
||||
$html .= '</div></td></tr></table>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function isSelected($i)
|
||||
{
|
||||
return is_int($i) && is_array($this->selected_values) && isset($this->selected_values[$i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the label of the field
|
||||
* @param string $for id of the target field
|
||||
* @return string the <div> (unclosed for captcha)
|
||||
*/
|
||||
private function htmlLabel($for)
|
||||
{
|
||||
$html = '<label for="' . $for . '">';
|
||||
if ($this->title) {
|
||||
$html .= $this->title;
|
||||
} else {
|
||||
$html .= ucfirst($this->form->lang($this->type));
|
||||
}
|
||||
if ($this->description) {
|
||||
$html .= ' <em class="description">' . $this->description . '</em>';
|
||||
}
|
||||
if ($this->error) {
|
||||
$html .= ' <span class="error-msg">' . $this->form->lang($this->error) . '</span>';
|
||||
}
|
||||
$html .= '</label>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the general type of a field, even of specials fields.
|
||||
*/
|
||||
private function getGeneralType()
|
||||
{
|
||||
$types = array(
|
||||
'name' => 'text',
|
||||
'subject' => 'text',
|
||||
'message' => 'textarea',
|
||||
'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>';
|
||||
if ($return) {
|
||||
return $out;
|
||||
}
|
||||
echo $out;
|
||||
}
|
||||
function predump($arr)
|
||||
{
|
||||
echo'<pre class="test" style="white-space:pre-wrap;">';
|
||||
var_dump($arr);
|
||||
echo'</pre>';
|
||||
}
|
||||
function unset_r($a, $i)
|
||||
{
|
||||
foreach ($a as $k => $v) {
|
||||
if (isset($v[$i])) {
|
||||
unset($a[$k][$i]);
|
||||
}
|
||||
}
|
||||
return $a;
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* p01-contact - A simple contact forms manager
|
||||
*
|
||||
* @link https://github.com/nliautaud/p01contact
|
||||
* @author Nicolas Liautaud
|
||||
* @package p01contact
|
||||
*/
|
||||
namespace P01C;
|
||||
|
||||
class Session
|
||||
{
|
||||
private static $key = 'p01contact';
|
||||
|
||||
private static function init()
|
||||
{
|
||||
if (session_id() === '') {
|
||||
session_start();
|
||||
}
|
||||
if (!self::exists()) {
|
||||
$_SESSION[self::$key] = [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return if the session data exists, or if the given key exists.$_COOKIE
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function exists($key = null)
|
||||
{
|
||||
$sessionExist = !empty($_SESSION) && !empty($_SESSION[self::$key]);
|
||||
if ($key === null) {
|
||||
return $sessionExist;
|
||||
}
|
||||
return $sessionExist && isset($_SESSION[self::$key][$key]);
|
||||
}
|
||||
/**
|
||||
* Set the given key to the given value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $val
|
||||
* @return void
|
||||
*/
|
||||
public static function set($key, $val)
|
||||
{
|
||||
if (!self::exists()) {
|
||||
self::init();
|
||||
}
|
||||
$_SESSION[self::$key][$key] = $val;
|
||||
}
|
||||
/**
|
||||
* Get the given key data.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default (optional) Value to return if the key doesn't exist.
|
||||
* @return mixed `$default` or `null`
|
||||
*/
|
||||
public static function get($key, $default = null)
|
||||
{
|
||||
if (!self::exists($key)) {
|
||||
return $default;
|
||||
}
|
||||
return $_SESSION[self::$key][$key];
|
||||
}
|
||||
/**
|
||||
* Add value to the array named key and shift old
|
||||
* entries until the array is of given size.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $val
|
||||
* @param integer $size
|
||||
* @return void
|
||||
*/
|
||||
public static function stack($key, $val, $size = 2)
|
||||
{
|
||||
if (!self::exists()) {
|
||||
self::init();
|
||||
}
|
||||
$arr = self::get($key);
|
||||
if (!isset($arr)) {
|
||||
$arr = [];
|
||||
}
|
||||
if (!is_array($arr)) {
|
||||
return;
|
||||
}
|
||||
array_push($arr, $val);
|
||||
while (count($arr) > $size) {
|
||||
array_shift($arr);
|
||||
}
|
||||
self::set($key, $arr);
|
||||
}
|
||||
/**
|
||||
* Return the session data, in html.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function report()
|
||||
{
|
||||
if (!self::exists()) {
|
||||
return;
|
||||
}
|
||||
$out = '<h3>$_SESSION :</h3>';
|
||||
$out.= preint($_SESSION, true);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<style>
|
||||
.p01contact_panel * {
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
}
|
||||
.p01contact_panel {
|
||||
overflow: auto;
|
||||
}
|
||||
.p01contact_panel .version {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
.p01contact_panel fieldset {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
margin: 2em 0;
|
||||
}
|
||||
.p01contact_panel .field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
.p01contact_panel .field > :first-child {
|
||||
width: 48%;
|
||||
}
|
||||
.p01contact_panel .field > :last-child {
|
||||
width: 48%;
|
||||
}
|
||||
.p01contact_panel textarea {
|
||||
height: 3em;
|
||||
}
|
||||
.p01contact_panel strong {
|
||||
display: block;
|
||||
}
|
||||
.p01contact_panel p {
|
||||
margin: 1em 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.p01contact_panel p,
|
||||
.p01contact_panel .field em {
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
font-size: .95em;
|
||||
font-style: normal;
|
||||
}
|
||||
.p01contact_panel em label {
|
||||
display: inline;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
padding: 0 .5em;
|
||||
}
|
||||
.p01contact_panel .text.left {
|
||||
width: 7em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
.p01contact_panel .submit {
|
||||
float: right;
|
||||
}
|
||||
.logs {
|
||||
margin-top: 5em;
|
||||
}
|
||||
.logs table {
|
||||
max-height: 30em;
|
||||
}
|
||||
</style>
|
||||
<div class="p01contact_panel">
|
||||
<form action="" method="post">
|
||||
<input class="submit" type="submit" value="Save settings" />
|
||||
<h3>{{lang.config_title}}</h3>
|
||||
<p>
|
||||
<a href="{{REPOURL}}">{{lang.repo}}</a> -
|
||||
<a href="{{WIKIURL}}">{{lang.wiki}}</a> -
|
||||
<a href="{{ISSUESURL}}">{{lang.issues}}</a>
|
||||
<span class="version">v{{version}}</span>
|
||||
</p>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.default_email}}</strong><em>{{lang.default_email_sub}}</em></div>
|
||||
<textarea name="p01-contact[settings][default_email]">config(default_email)</textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.lang}}</strong><em>{{lang.lang_sub}} {{default_lang}}</em></div>
|
||||
<select class="text" name="p01-contact[settings][lang]">{{langsoptions}}</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.message_len}}</strong><em>{{lang.message_len_sub}}</em></div>
|
||||
<input type="number" class="text" name="p01-contact[settings][message_len]" value="config(message_len)" size=3 maxlength=3 min=0 />
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.default_params}}</strong><em>{{lang.default_params_sub}}</em></div>
|
||||
<textarea name="p01-contact[settings][default_params]">config(default_params)</textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.separator}}</strong><em>{{lang.separator_sub}}</em></div>
|
||||
<input type="text" class="text" name="p01-contact[settings][separator]" value="config(separator)"/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.logs_count}}</strong><em>{{lang.logs_count_sub}}</em></div>
|
||||
<input type="number" class="text" name="p01-contact[settings][logs_count]" value="config(logs_count)" min=0 />
|
||||
</label>
|
||||
<fieldset>
|
||||
<h3>{{lang.checklists}}</h3>
|
||||
<p>{{lang.checklists_sub}}</p>
|
||||
<div class="field">
|
||||
<div>
|
||||
<input type="text" class="text left" name="p01-contact[settings][checklist][0][name]" value="config(checklist,0,name)"/>
|
||||
<em>
|
||||
<label><input name="p01-contact[settings][checklist][0][type]" type="radio" value="blacklist" {{cl0bl}}/> {{lang.blacklist}}</label>
|
||||
<label><input name="p01-contact[settings][checklist][0][type]" type="radio" value="whitelist" {{cl0wh}}/> {{lang.whitelist}}</label>
|
||||
</em>
|
||||
</div>
|
||||
<textarea name="p01-contact[settings][checklist][0][content]">config(checklist,0,content)</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div>
|
||||
<input type="text" class="text left" name="p01-contact[settings][checklist][1][name]" value="config(checklist,1,name)"/>
|
||||
<em>
|
||||
<label><input name="p01-contact[settings][checklist][1][type]" type="radio" value="blacklist" {{cl1bl}}/> {{lang.blacklist}}</label>
|
||||
<label><input name="p01-contact[settings][checklist][1][type]" type="radio" value="whitelist" {{cl1wh}}/> {{lang.whitelist}}</label>
|
||||
</em>
|
||||
</div>
|
||||
<textarea name="p01-contact[settings][checklist][1][content]">config(checklist,1,content)</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div>
|
||||
<input type="text" class="text left" name="p01-contact[settings][checklist][2][name]" value="config(checklist,2,name)"/>
|
||||
<em>
|
||||
<label><input name="p01-contact[settings][checklist][2][type]" type="radio" value="blacklist" {{cl2bl}}/> {{lang.blacklist}}</label>
|
||||
<label><input name="p01-contact[settings][checklist][2][type]" type="radio" value="whitelist" {{cl2wh}}/> {{lang.whitelist}}</label>
|
||||
</em>
|
||||
</div>
|
||||
<textarea name="p01-contact[settings][checklist][2][content]">config(checklist,2,content)</textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<h3>{{lang.Security}}</h3>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.use_honeypot}}</strong><em>{{lang.use_honeypot_sub}}</em></div>
|
||||
<input type="checkbox" name="p01-contact[settings][use_honeypot]" {{honeypotchecked}}/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.min_sec_after_load}}</strong><em>{{lang.min_sec_after_load_sub}}</em></div>
|
||||
<input type="number" class="text" name="p01-contact[settings][min_sec_after_load]" value="config(min_sec_after_load)" min=0 />
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.min_sec_between_posts}}</strong><em>{{lang.min_sec_between_posts_sub}}</em></div>
|
||||
<input type="number" class="text" name="p01-contact[settings][min_sec_between_posts]" value="config(min_sec_between_posts)" min=0 />
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.max_posts_by_hour}}</strong><em>{{lang.max_posts_by_hour_sub}}</em></div>
|
||||
<input type="number" class="text" name="p01-contact[settings][max_posts_by_hour]" value="config(max_posts_by_hour)" min=0 />
|
||||
</label>
|
||||
<p>{{lang.captcha_info}}</p>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.recaptcha_public_key}}</strong><em>{{lang.recaptcha_public_key_sub}}</em> (<a href=\"https://www.google.com/recaptcha/admin\">reCaptcha admin</a>)</div>
|
||||
<input type="text" class="text" name="p01-contact[settings][recaptcha_public_key]" value="config(recaptcha_public_key)"/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.recaptcha_secret_key}}</strong><em>{{lang.recaptcha_secret_key_sub}}</em> (<a href=\"https://www.google.com/recaptcha/admin\">reCaptcha admin</a>)</div>
|
||||
<input type="text" class="text" name="p01-contact[settings][recaptcha_secret_key]" value="config(recaptcha_secret_key)"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<h3>{{lang.debug}}</h3>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.disable}}</strong><em>{{lang.disable_sub}}</em></div>
|
||||
<input type="checkbox" name="p01-contact[settings][disable]" {{disablechecked}}/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<div><strong>{{lang.debug}}</strong><em>{{lang.debug_sub}} {{lang.debug_warn}}</em></div>
|
||||
<input type="checkbox" name="p01-contact[settings][debug]" {{debugchecked}}/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<input class="submit" type="submit" value="Save settings" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<table width="100%" style="min-height: 100%; max-width: 40em; margin: auto">
|
||||
<tr style="height: 3em;">
|
||||
<td colspan="3">
|
||||
<h2 style='margin: .5em 0; font-size: 1.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;'>
|
||||
{{lang.email_title}}
|
||||
<a href="{{PAGEURL}}">{{SERVERNAME}}</a>
|
||||
</h2>
|
||||
</td>
|
||||
<tr style="height: 2em; font-size: .875em; color: #888888">
|
||||
<td style="text-align: left">{{date}}</td>
|
||||
<td style="text-align: center"><a href="{{PAGEURL}}" style="color:#888888">{{PAGEURI}}</a></td>
|
||||
<td style="text-align: right">{{ip}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" style="padding: 4em 0">{{fields}}</td>
|
||||
</tr>
|
||||
<tr style="height: 2em; font-size: .875em">
|
||||
<td colspan="2">
|
||||
If this mail should not be for you, please contact
|
||||
<a href="mailto:{{contact}}">{{contact}}</a>.
|
||||
</td>
|
||||
<td style="text-align:right">
|
||||
<a style="color: #cccccc; text-decoration: none" href="{{REPOURL}}">
|
||||
p01-contact v{{VERSION}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
+1161
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user