Blame | Last modification | View Log | Download
<?php namespace ProcessWire;use \Jos\SpamProtection;use \Jos\Mailer;/**** SimpleContactForm** See README.md for usage instructions.** @author Tabea David <info@justonestep.de>* @version 1.0.7* @copyright Copyright (c) 2017* @see https://github.com/justonestep/processwire-simplecontactform* @see http://www.processwire.com*//*** Class SimpleContactForm*/class SimpleContactForm extends WireData implements Module {/*** Get module information** @return array*/public static function getModuleInfo() {return array('title' => 'Simple Contact Form','summary' => 'Just a simple contact form.','version' => 107,'href' => 'https://github.com/justonestep/processwire-simplecontactform','singular' => true,'autoload' => true,'icon' => 'envelope',);}/*** string class name*/const CLASS_NAME = 'SimpleContactForm';/*** string template name*/const TEMPLATE_NAME = 'simple_contact_form';/*** string template name for save messages*/const SM_TEMPLATE_NAME = 'simple_contact_form_messages';/*** string tag name*/const TAG_NAME = 'scf';/*** Additional fields*/static protected $additionalFields = array('date', 'ip');/*** Spam protection fields*/static protected $spamFields = array('scf-date', 'scf-website', 'sumbit', 'token');/*** Markup used during the render() method*/static protected $markup = array('list' => "{out}\n",'item' => "\n\t<div {attrs}>\n{out}\n\t</div>",'item_label' => "\n\t\t<label class='form__item--label' for='{for}'>{out}</label>",'item_label_hidden' => "\n\t\t<label class='field__header field__header--hidden {class}'>{out}</label>",'item_content' => "{out}",'item_error' => "\n<p class='field--error--message'>{out}</p>",'item_description' => "\n<p class='field__description'>{out}</p>",'item_head' => "\n<h2>{out}</h2>",'item_notes' => "\n<p class='field__notes'>{out}</p>",'item_icon' => "",'item_toggle' => "",// ALSO:// InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis));/*** Classes used during the render() method*/static protected $classes = array('form' => 'form js-simplecontactform', // additional clases for inputfieldform (optional)'form_error' => 'form--error--message','form_success' => 'form--success--message','list' => 'fields','list_clearfix' => 'clearfix','item' => 'form__item form__item--{name}','item_label' => '', // additional classes for inputfieldheader (optional)'item_content' => '', // additional classes for inputfieldcontent (optional)'item_required' => 'field--required', // class is for inputfield'item_error' => 'field--error', // note: not the same as markup[item_error], class is for inputfield'item_collapsed' => 'field--collapsed','item_column_width' => 'field__column','item_column_width_first' => 'field__column--first','item_show_if' => 'field--show-if','item_required_if' => 'field--required-if'// ALSO:// InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis));/*** construct*/public function __construct() {// require spam librequire_once($this->config->paths->SimpleContactForm . 'lib/SpamProtection.php');require_once($this->config->paths->SimpleContactForm . 'lib/Mailer.php');// add log file$this->log = new FileLog($this->config->paths->logs . strtolower(self::CLASS_NAME) . '-log.txt');}/*** Initialize the module** ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called* when ProcessWire's API is ready. As a result, this is a good place to attach hooks.*/public function init() {$allFieldsExtended = $this->allFields;foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f;$this->allFieldsExtended = $allFieldsExtended;$this->submitName = 'submit';$this->btnClass = 'button';$this->btnText = $this->_('Send');}/*** Initialize the module - ready** ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called* when ProcessWire's API is ready. As a result, this is a good place to attach hooks.*/public function ready() {$this->errorMessage = $this->_('Please verify the data you have entered.');$this->successMessage = $this->_('Your contact request has been sent successfully!');$this->addHookBefore('Modules::saveModuleConfigData', $this, 'createAndAddFieldsOnSaveModuleConfigData');}/*** Set markup** @param Form $form* @param array $options*/private function setMarkup(&$form, $options) {$markup = isset($options['markup']) ? array_merge(self::$markup, $options['markup']) : self::$markup;$form->setMarkup($markup);}/*** Set classes** @param Form $form* @param array $options*/private function setClasses(&$form, $options) {$classes = isset($options['classes']) ? array_merge(self::$classes, $options['classes']) : self::$classes;$form->setClasses($classes);}/*** Render Success Message** @param Form $form* @return string*/private function renderSuccessMessage($form) {return "<p class='{$form->getClasses()['form_success']}'>{$this->successMessage}</p>";}/*** Render Form** @param array $options* @return string*/private function renderForm($options) {$form = $this->getForm($options);$this->setMarkup($form, $options);$this->setClasses($form, $options);// on form submitif ($this->input->post->{$this->submitName} && !$this->input->get->success) {// process form input and validate fields$form->processInput($this->input->post);$this->processValidation($form);// anti spam measuresif (!$form->getErrors()) {$spamProtector = new SpamProtection();// exclude markup fields from spam count// AND exclude checkbox fields (if unchecked) from spam count// AND exclude password confirmation field from spam count BUT the other way// AND exclude verify email BUT the other way// BECAUSE it's a standard browser behaviour that the value of a checkbox is only sent if the checkbox was checked$excludeFields = 0;foreach ($this->allFields as $inputfield) {if ($field = $this->fields->get($inputfield)) {if ($field->type instanceof FieldtypeFieldsetOpen || $field->type instanceof FieldtypeFieldsetTabOpen) $excludeFields++;if ($field->type instanceof FieldtypeCheckbox && !$this->input->post->{$field->name}) $excludeFields++;if ($field->type instanceof FieldtypePassword) $excludeFields--;if ($field->type instanceof FieldtypeEmail && $field->confirm) $excludeFields--;}}$spamProtector->setCount(count($this->allFields) - $excludeFields + count(self::$spamFields))->setTimeRange($this->antiSpamTimeMin, $this->antiSpamTimeMax)->setSaveMessages($this->saveMessages)->setExcludeIpAdresses($this->antiSpamExcludeIps)->setNumberOfSubmitsPerDay($this->antiSpamPerDay)->validate();// didn't pass spam testif ($spamProtector->isSpam()) {$form->error(sprintf($this->_("Sorry, but your message didn't pass our %s test. Please prepare another %s."),$spamProtector->getAnimal(),$spamProtector->getFruit()));$form->appendMarkup .= "<p class='{$form->getClasses()['form_error']}'>{$form->getErrors()[0]}</p>";$d = $form->get('scf-date');$d->attr('value', time());}} else {$form->appendMarkup .= "<p class='{$form->getClasses()['form_error']}'>{$this->errorMessage}</p>";}// send mailif (!$form->getErrors()) {if ($this->sendEmails) $this->sendMail();if ($this->saveMessages) $this->saveMessage();if ($this->redirectSamePage) {$this->session->redirect($this->page->url . '?success=1');} elseif ($p = $this->redirectPage) {$this->session->redirect($this->pages->get($p)->url);} else {$out = $this->renderSuccessMessage($form);}}}if ($this->input->get->success) $out = $this->renderSuccessMessage($form);return isset($out) ? $out : $form->render();}/*** Get form** @param array $options* @return Form*/private function getForm($options) {$form = $this->modules->get('InputfieldForm');$form->action = isset($options['action']) ? $options['action'] : './';$form->method = 'post';$form->attr('id+name','contact-form');if (isset($options['prependMarkup'])) $form->prependMarkup = $options['prependMarkup'];if (isset($options['appendMarkup'])) $form->appendMarkup = $options['appendMarkup'];// add fieldsif (is_array($this->allFields)) {foreach ($this->allFields as $fieldname) {if ($field = $this->fields->get($fieldname)) {$inputfield = $field->getInputfield($this->page);$inputfield->useLanguages = false;$form->append($inputfield);}}}// add honeypot (spam protection)$honeyField = $this->modules->get('InputfieldText');$honeyField->name = 'scf-website';$honeyField->initValue = '';$form->append($honeyField);// add hidden field to save current timestamp$field = $this->modules->get('InputfieldHidden');$field->skipLabel = Inputfield::skipLabelBlank;$field->attr('name+id', 'scf-date');$field->attr('value', time());$field->required = 1;$form->append($field);// add a submit button to the form$submit = $this->modules->get('InputfieldSubmit');$submit->name = $this->submitName;$submit->attr('class', $this->btnClass);$submit->attr('value', $this->btnText === 'Send' ? $this->_('Send') : $this->btnText);$form->append($submit);return $form;}/*** Render* available keys:* allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText* emailMessage, emailAddMessage, successMessage, errorMessage* sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo* saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup** @param array $options* @return string*/public function ___render($options = array()) {return $this->renderInstance($options);}/*** Render further instance* available keys:* allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText* emailMessage, emailAddMessage, successMessage, errorMessage* sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo* saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup** @param array $options* @return string*/private function renderInstance($options) {// overwrite module config settingsforeach ($options as $key => $value) {switch ($key) {// === fieldscase 'allFields':$this->allFields = explode(',', $value);$allFieldsExtended = explode(',', $value);foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f;$this->allFieldsExtended = $allFieldsExtended;break;case 'redirectSamePage':$this->redirectSamePage = $value;break;case 'redirectPage':$this->redirectPage = $value;break;// === namescase 'submitName':$this->submitName = $value;break;case 'btnClass':$this->btnClass = $value;break;case 'btnText':$this->btnText = $value;break;// === messagescase 'successMessage':$this->successMessage = $value;break;case 'errorMessage':$this->errorMessage = $value;break;case 'emailMessage':$this->emailMessage = $value;break;case 'emailAddMessage':$this->emailAddMessage = $value;break;case 'sendEmails':$this->sendEmails = $value;break;case 'emailSubject':$this->emailSubject = $value;break;case 'emailTo':$this->emailTo = $value;break;case 'emailServer':$this->emailServer = $value;break;case 'emailReplyTo':$this->emailReplyTo = $value;break;case 'emailAdd':$this->emailAdd = $value;break;case 'emailAddSubject':$this->emailAddSubject = $value;break;case 'emailAddTo':$this->emailAddTo = $value;break;case 'emailAddReplyTo':$this->emailAddReplyTo = $value;break;// === generalcase 'saveMessages':$this->saveMessages = $this->boolval($value);break;case 'saveMessagesParent':$this->saveMessagesParent = $value;break;case 'saveMessagesTemplate':$this->saveMessagesTemplate = $value;break;}}// send additional email?if (isset($this->emailAdd) && $this->emailAdd) {if (!isset($this->emailAddMessage)) $this->emailAddMessage = $this->emailMessage;if (!isset($this->emailAddTo)) $this->emailAddTo = $this->emailTo;if (!isset($this->emailAddReplyTo)) $this->emailAddReplyTo = $this->emailReplyTo;if (!isset($this->emailAddSubject)) $this->emailAddSubject = $this->emailSubject;}// execute renderreturn $this->renderForm($options);}/*** Get mail message content** @param string $text* @return string*/private function getMessageContent($text) {if (!empty($text)) {$date = new \DateTime();if (preg_match('/\%date\%/', $text)) $text = str_replace('%date%', $date->format('Y-m-d H:i:s'), $text);preg_match_all('/\%(.*?)\%/', $text, $matches);foreach ($matches[0] as $key => $match) {$text = str_replace($match, $this->sanitizer->textarea($this->input->post->{$matches[1][$key]}), $text);}} else {$message = array();foreach ($this->allFields as $inputfield) {$message[] = $inputfield . ': ' . $this->sanitizer->textarea($this->input->post->{$inputfield});}$date = new \DateTime();$message[] = 'Date: ' . $date->format('Y-m-d H:i:s');$text = implode("\r\n", $message);}return $text;}/*** Send Mail*/public function ___sendMail() {$mail = new Mailer($this->emailTo,$this->emailServer,$this->emailReplyTo,$this->emailSubject,trim($this->getMessageContent($this->emailMessage)));$numSent = $mail->send();// send additional mailif ($this->sendEmails && isset($this->emailAdd) && $this->emailAdd) $this->sendAdditionalMail();// log whether a mail has been sent or notif ($numSent) {$logmessage = array($_SERVER['HTTP_USER_AGENT'],$_SERVER['REMOTE_ADDR'],$this->emailTo);$this->log->save('[SUCCESS] ' . implode(', ', $logmessage));} else {// mail has not been sent$this->log->save("[ERROR] Mail has not been sent to {$this->emailTo}");}}/*** Save Message*/public function ___saveMessage() {$date = new \DateTime();$name = array($date->getTimestamp());if ($parts = $this->saveMessagesScheme) {foreach ($parts as $part) {$name[] = $this->input->post->$part;}}$pageName = implode(' ', $name);$p = new Page();$p->template = $this->saveMessagesTemplate;$p->parent = wire('pages')->get($this->saveMessagesParent);$p->name = $this->sanitizer->pageName($pageName, true);$p->title = $pageName;$p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below!foreach ($this->allFields as $in) $p->$in = $this->input->post->$in;$p->scf_date = $date->getTimestamp();$p->scf_ip = $_SERVER['REMOTE_ADDR'];$p->save();}/*** Send Additional Mail*/public function ___sendAdditionalMail() {$mail = new Mailer($this->emailAddTo,$this->emailServer,$this->emailAddReplyTo,$this->emailAddSubject,trim($this->getMessageContent($this->emailAddMessage)));if ($mail->send()) {$logmessage = array($_SERVER['HTTP_USER_AGENT'],$_SERVER['REMOTE_ADDR'],$this->emailAddTo);$this->log->save('[SUCCESS] ' . implode(', ', $logmessage));} else {$this->log->save("[ERROR] Additional mail could not be sent to {$this->emailAddTo}");}}/*** Hook create and add template fields** @param HookEvent $event*/public function createAndAddFieldsOnSaveModuleConfigData(HookEvent $event) {if ($event->arguments[0] === self::CLASS_NAME) {$configData = $event->arguments[1];// saveMessages enabled? create template if it doesn't exist$fg = $configData['saveMessages'] ? $this->createSaveMessagesTemplate($configData) : null;// get fieldsif ($configData['addFields']) {$this->addNewFields($configData, $fg);$event->setArgument(1, $configData);}// cleanup, update from 0.x to 1.x @todo: deprecated$this->upgradeItems($fg, $configData);}}/*** Add new fields** @param array $configData* @param Fieldgroup $fg*/protected function addNewFields(&$configData, $fg) {$newFields = $configData['addFields'];$allFields = $configData['allFields'];foreach (explode(',', preg_replace('/\s/', '', $newFields)) as $name) {if (is_null($this->fields->get("scf_$name"))) {$f = new Field();$f->type = $this->modules->get('FieldtypeText');$f->name = "scf_$name";$f->label = 'SCF - ' . ucfirst($name);$f->tags = self::TAG_NAME;$f->columnWidth = '25';$f->save();// saveMessages enabled - save fields to templateif ($fg) {$fg->add($f); // add field to fieldgroup$fg->save(); // save fieldgroup}}if (!in_array("scf_$name", $allFields)) $allFields[] = "scf_$name";}$configData['allFields'] = $allFields;$configData['addFields'] = '';}/*** Create save messages template** @param array $configData* @return Fieldgroup*/protected function createSaveMessagesTemplate($configData) {if ($template = $this->templates->get(self::SM_TEMPLATE_NAME)) {$fg = $template->fieldgroup; // get existing fieldgroup} else {// new fieldgroup$fg = new Fieldgroup();$fg->name = self::SM_TEMPLATE_NAME;$fg->add($this->fields->get('title')); // needed title field$fg->save();// new template$template = new Template();$template->name = self::SM_TEMPLATE_NAME;$template->fieldgroup = $fg; // add the fieldgroup$template->slashUrls = 1;$template->noPrependTemplateFile = 1;$template->noAppendTemplateFile = 1;$template->tags = self::TAG_NAME;$template->save();}// scf_spamip (check)if (!$fg->scf_spamip) {if (!$field = $this->fields->get('scf_spamIp')) {$field = new Field();$field->type = $this->modules->get('FieldtypeCheckbox');$field->name = 'scf_spamIp';$field->value = 1;}$field->label = __('Add IP to spam list');$field->description = __('If you activate this checkbox, further contact requests from this ip address will be treated as spam.');$field->columnWidth = 25;$field->save();$fg->add($field);$fg->save();}// scf_date (datetime)if (!$fg->scf_date) {if (!$field = $this->fields->get('scf_date')) {$field = new Field();$field->name = 'scf_date';}$field->type = $this->modules->get('FieldtypeDatetime');$field->label = __('Creation date');$field->datepicker = InputfieldDatetime::datepickerClick;$field->dateInputFormat = 'Y/m/d';$field->timeInputFormat = 'H:i';$field->dateOutputFormat = 'Y/m/d';$field->timeOutputFormat = 'H:i';$field->columnWidth = 25;$field->save();$fg->add($field);$fg->save();}// scf_ip (text)if (!$fg->scf_ip) {if (!$field = $this->fields->get('scf_ip')) {$field = new Field();$field->type = $this->modules->get('FieldtypeText');$field->name = 'scf_ip';}$field->label = __('IP address');$field->columnWidth = 25;$field->save();$fg->add($field);$fg->save();}$this->validateParent($configData);return $fg;}/*** Validate parent page** @param array $configData*/protected function validateParent($configData) {// add default for save messages parentif (!$configData['saveMessagesParent']) $configData['saveMessagesParent'] = $this->config->rootPageID;// check whether selected parent allows children (noChildren must be 0)if ($this->pages->get($configData['saveMessagesParent'])->template->noChildren > 0) {$this->log->error($this->_('Please choose another parent or change the belonging template. It must allow children.'));}}/*** Validate parent page** @param Fieldgroup $fg* @param array $configData*/protected function upgradeItems($fg, $configData) {if ($this->fields->get('repeater_scfmessages') && $configData['saveMessages']) {// first: add fields to templateforeach ($this->allFields as $f) {$fg->add($f); // add field to fieldgroup$fg->save(); // save fieldgroup}// second: repater items to pagesforeach ($this->pages->get('template=' . self::SM_TEMPLATE_NAME)->repeater_scfmessages as $item) {if (!$item->scf_date) continue;$name = array($item->created);if ($parts = $configData['saveMessagesScheme']) {foreach ($parts as $part) {$name[] = $item->$part;}}$pageName = implode(' ', $name);if ($this->pages->find('name=' . $this->sanitizer->pageName($pageName, true))->count() > 0) continue;$p = new Page();$p->template = self::SM_TEMPLATE_NAME;$p->parent = wire('pages')->get($configData['saveMessagesParent']);$p->name = $this->sanitizer->pageName($pageName, true);$p->title = $pageName;$p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below!$date = new \DateTime();$date->setTimestamp($item->created);$p->scf_date = $date->format('Y/m/d H:i');$p->scf_ip = $item->scf_ip;foreach ($this->allFields as $f) if ($item->$f) $p->$f = $item->$f;$p->save();}// third: delete repeater and template simple_contact_form$fg->remove($this->fields->get('repeater_scfmessages'));$fg->save();$this->fields->delete($this->fields->get('repeater_scfmessages'));$this->templates->delete($this->templates->get(self::TEMPLATE_NAME));}}/*** Get the boolean value of a variable** @param $val*/public function boolval($val) {if (!function_exists('boolval')) {// (PHP 5 < 5.5.0)$bool = (bool) $val;} else {// (PHP 5 >= 5.5.0)$bool = boolval($val);}return $bool;}/*** Hookable method called after the form was processed* Allows custom/extra validation and field manipulation*/protected function ___processValidation($form) {}}