├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── SimpleContactForm.module
├── SimpleContactFormConfig.php
├── doc
├── overwrite-classes-and-markup.md
├── spam-translate.md
└── success-message.md
├── lib
├── Mailer.php
└── SpamProtection.php
└── resources
├── contact.js
└── jquery.simplecontactform.js
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 1.0.8 (2018-01-07)
4 |
5 | - add option to redirect to same page to prevent form resubmission
6 |
7 | ### 1.0.7 (2017-11-29)
8 |
9 | - exclude email and password confirmation fields from spam count
10 |
11 | ### 1.0.6 (2017-11-01)
12 |
13 | - add possibility to specify a template in which the form data should be saved
14 |
15 | ### 1.0.5 (2017-09-26)
16 |
17 | - exclude checkbox fields (if unchecked) from spam count
18 | - because it's a standard browser behaviour that the value of a checkbox is only sent if the checkbox was checked
19 | - this leads to a mismatch while counting fields
20 | - the number of submitted fields does not match the number of fields which are present in the form
21 |
22 | ### 1.0.4 (2017-09-14)
23 |
24 | - extend option `classes`
25 | - allows customization of classes for form error and success
26 |
27 | ### 1.0.3 (2017-05-14)
28 |
29 | - corrects typo in success message: **The translation string has been changed, some may need to translate it again!** // thanks @szabesz
30 | - hides date field label // thanks @binarious
31 | - makes send btn text translatable
32 |
33 | ### 1.0.2 (2017-03-18)
34 |
35 | - adds option to specify a redirect page
36 |
37 | ### 1.0.1 (2016-12-16)
38 |
39 | - adds setting `sendEmails`, define whether Emails should be sent
40 |
41 | ### 1.0.0 (2016-03-23)
42 |
43 | - adds ProcessWire 3.x compatibility. Choose branch `2.x` if you want to use it with a version below 3.x
44 | - outsources Mailer and SpamProtection
45 | - adds namespaces support
46 | - adds a Reply-To-Header (optional)
47 |
48 | ### 0.2.1 (2016-01-10)
49 |
50 | - data will be stored, regardless whether a mail has been sent or not
51 | - save corresponding log entry
52 |
53 | ### 0.2.0 (2016-01-06)
54 |
55 | - supports multiple instances
56 | - adds usage of full ProcessWire API in options
57 | - allows to render more than one contact form on a page
58 | - allows to send more than one email
59 | - makes validation hookable
60 |
61 | ### 0.1.2 (2015-08-17)
62 |
63 | - fixes CSRF Token validation
64 | - allows to overwrite mail template
65 | - allows multiple email recipients
66 |
67 | ### 0.1.1 (2015-04-09)
68 |
69 | - updates template handling
70 |
71 | ### 0.1.0 (2015-03-24)
72 |
73 | - additional spam protection and logging
74 |
75 | ### 0.0.9 (2015-03-09)
76 |
77 | - little bugfixes
78 |
79 | ### 0.0.8 (2015-01-22)
80 |
81 | - uses `wireMail` instead of php mail function
82 |
83 | ### 0.0.7 (2014-11-18)
84 |
85 | - adding spam protection comparing post fields
86 |
87 | ### 0.0.6 (2014-11-11)
88 |
89 | - adds php template support
90 |
91 | ### 0.0.5 (2014-10-05)
92 |
93 | - extends anti spam functionality
94 |
95 | ### 0.0.4 (2014-09-30)
96 |
97 | - adds spam protection, honeypot as well as timestamp comparison
98 |
99 | ### 0.0.3 (2014-08-18)
100 |
101 | - save received messages
102 |
103 | ### 0.0.1 (2014-07-11)
104 |
105 | - initial module
106 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
{out}
", 82 | 'item_head' => "\n{out}
", 84 | 'item_icon' => "", 85 | 'item_toggle' => "", 86 | // ALSO: 87 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 88 | ); 89 | 90 | /** 91 | * Classes used during the render() method 92 | */ 93 | static protected $classes = array( 94 | 'form' => 'form js-simplecontactform', // additional clases for inputfieldform (optional) 95 | 'form_error' => 'form--error--message', 96 | 'form_success' => 'form--success--message', 97 | 'list' => 'fields', 98 | 'list_clearfix' => 'clearfix', 99 | 'item' => 'form__item form__item--{name}', 100 | 'item_label' => '', // additional classes for inputfieldheader (optional) 101 | 'item_content' => '', // additional classes for inputfieldcontent (optional) 102 | 'item_required' => 'field--required', // class is for inputfield 103 | 'item_error' => 'field--error', // note: not the same as markup[item_error], class is for inputfield 104 | 'item_collapsed' => 'field--collapsed', 105 | 'item_column_width' => 'field__column', 106 | 'item_column_width_first' => 'field__column--first', 107 | 'item_show_if' => 'field--show-if', 108 | 'item_required_if' => 'field--required-if' 109 | // ALSO: 110 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 111 | ); 112 | 113 | /** 114 | * construct 115 | */ 116 | public function __construct() { 117 | // require spam lib 118 | require_once($this->config->paths->SimpleContactForm . 'lib/SpamProtection.php'); 119 | require_once($this->config->paths->SimpleContactForm . 'lib/Mailer.php'); 120 | 121 | // add log file 122 | $this->log = new FileLog($this->config->paths->logs . strtolower(self::CLASS_NAME) . '-log.txt'); 123 | } 124 | 125 | /** 126 | * Initialize the module 127 | * 128 | * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called 129 | * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 130 | */ 131 | public function init() { 132 | $allFieldsExtended = $this->allFields; 133 | foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f; 134 | $this->allFieldsExtended = $allFieldsExtended; 135 | $this->submitName = 'submit'; 136 | $this->btnClass = 'button'; 137 | $this->btnText = $this->_('Send'); 138 | } 139 | 140 | /** 141 | * Initialize the module - ready 142 | * 143 | * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called 144 | * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 145 | */ 146 | public function ready() { 147 | $this->errorMessage = $this->_('Please verify the data you have entered.'); 148 | $this->successMessage = $this->_('Your contact request has been sent successfully!'); 149 | 150 | $this->addHookBefore('Modules::saveModuleConfigData', $this, 'createAndAddFieldsOnSaveModuleConfigData'); 151 | } 152 | 153 | /** 154 | * Set markup 155 | * 156 | * @param Form $form 157 | * @param array $options 158 | */ 159 | private function setMarkup(&$form, $options) { 160 | $markup = isset($options['markup']) ? array_merge(self::$markup, $options['markup']) : self::$markup; 161 | $form->setMarkup($markup); 162 | } 163 | 164 | /** 165 | * Set classes 166 | * 167 | * @param Form $form 168 | * @param array $options 169 | */ 170 | private function setClasses(&$form, $options) { 171 | $classes = isset($options['classes']) ? array_merge(self::$classes, $options['classes']) : self::$classes; 172 | $form->setClasses($classes); 173 | } 174 | 175 | /** 176 | * Render Success Message 177 | * 178 | * @param Form $form 179 | * @return string 180 | */ 181 | private function renderSuccessMessage($form) { 182 | return "{$this->successMessage}
"; 183 | } 184 | 185 | /** 186 | * Render Form 187 | * 188 | * @param array $options 189 | * @return string 190 | */ 191 | private function renderForm($options) { 192 | $form = $this->getForm($options); 193 | $this->setMarkup($form, $options); 194 | $this->setClasses($form, $options); 195 | 196 | // on form submit 197 | if ($this->input->post->{$this->submitName} && !$this->input->get->success) { 198 | // process form input and validate fields 199 | $form->processInput($this->input->post); 200 | $this->processValidation($form); 201 | 202 | // anti spam measures 203 | if (!$form->getErrors()) { 204 | $spamProtector = new SpamProtection(); 205 | 206 | // exclude markup fields from spam count 207 | // AND exclude checkbox fields (if unchecked) from spam count 208 | // AND exclude password confirmation field from spam count BUT the other way 209 | // AND exclude verify email BUT the other way 210 | // BECAUSE it's a standard browser behaviour that the value of a checkbox is only sent if the checkbox was checked 211 | $excludeFields = 0; 212 | foreach ($this->allFields as $inputfield) { 213 | if ($field = $this->fields->get($inputfield)) { 214 | if ($field->type instanceof FieldtypeFieldsetOpen || $field->type instanceof FieldtypeFieldsetTabOpen) $excludeFields++; 215 | if ($field->type instanceof FieldtypeCheckbox && !$this->input->post->{$field->name}) $excludeFields++; 216 | if ($field->type instanceof FieldtypePassword) $excludeFields--; 217 | if ($field->type instanceof FieldtypeEmail && $field->confirm) $excludeFields--; 218 | } 219 | } 220 | 221 | $spamProtector 222 | ->setCount(count($this->allFields) - $excludeFields + count(self::$spamFields)) 223 | ->setTimeRange($this->antiSpamTimeMin, $this->antiSpamTimeMax) 224 | ->setSaveMessages($this->saveMessages) 225 | ->setExcludeIpAdresses($this->antiSpamExcludeIps) 226 | ->setNumberOfSubmitsPerDay($this->antiSpamPerDay) 227 | ->validate(); 228 | 229 | // didn't pass spam test 230 | if ($spamProtector->isSpam()) { 231 | $form->error(sprintf( 232 | $this->_("Sorry, but your message didn't pass our %s test. Please prepare another %s."), 233 | $spamProtector->getAnimal(), 234 | $spamProtector->getFruit() 235 | )); 236 | $form->appendMarkup .= "{$form->getErrors()[0]}
"; 237 | $d = $form->get('scf-date'); 238 | $d->attr('value', time()); 239 | } 240 | } else { 241 | $form->appendMarkup .= "{$this->errorMessage}
"; 242 | } 243 | 244 | // send mail 245 | if (!$form->getErrors()) { 246 | if ($this->sendEmails) $this->sendMail(); 247 | if ($this->saveMessages) $this->saveMessage(); 248 | 249 | if ($this->redirectSamePage) { 250 | $this->session->redirect($this->page->url . '?success=1'); 251 | } elseif ($p = $this->redirectPage) { 252 | $this->session->redirect($this->pages->get($p)->url); 253 | } else { 254 | $out = $this->renderSuccessMessage($form); 255 | } 256 | } 257 | } 258 | 259 | if ($this->input->get->success) $out = $this->renderSuccessMessage($form); 260 | 261 | return isset($out) ? $out : $form->render(); 262 | } 263 | 264 | /** 265 | * Get form 266 | * 267 | * @param array $options 268 | * @return Form 269 | */ 270 | private function getForm($options) { 271 | $form = $this->modules->get('InputfieldForm'); 272 | 273 | $form->action = isset($options['action']) ? $options['action'] : './'; 274 | $form->method = 'post'; 275 | $form->attr('id+name','contact-form'); 276 | if (isset($options['prependMarkup'])) $form->prependMarkup = $options['prependMarkup']; 277 | if (isset($options['appendMarkup'])) $form->appendMarkup = $options['appendMarkup']; 278 | 279 | // add fields 280 | if (is_array($this->allFields)) { 281 | foreach ($this->allFields as $fieldname) { 282 | if ($field = $this->fields->get($fieldname)) { 283 | $inputfield = $field->getInputfield($this->page); 284 | $inputfield->useLanguages = false; 285 | $form->append($inputfield); 286 | } 287 | } 288 | } 289 | 290 | // add honeypot (spam protection) 291 | $honeyField = $this->modules->get('InputfieldText'); 292 | $honeyField->name = 'scf-website'; 293 | $honeyField->initValue = ''; 294 | $form->append($honeyField); 295 | 296 | // add hidden field to save current timestamp 297 | $field = $this->modules->get('InputfieldHidden'); 298 | $field->skipLabel = Inputfield::skipLabelBlank; 299 | $field->attr('name+id', 'scf-date'); 300 | $field->attr('value', time()); 301 | $field->required = 1; 302 | $form->append($field); 303 | 304 | // add a submit button to the form 305 | $submit = $this->modules->get('InputfieldSubmit'); 306 | $submit->name = $this->submitName; 307 | $submit->attr('class', $this->btnClass); 308 | $submit->attr('value', $this->btnText === 'Send' ? $this->_('Send') : $this->btnText); 309 | $form->append($submit); 310 | 311 | return $form; 312 | } 313 | 314 | /** 315 | * Render 316 | * available keys: 317 | * allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText 318 | * emailMessage, emailAddMessage, successMessage, errorMessage 319 | * sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo 320 | * saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup 321 | * 322 | * @param array $options 323 | * @return string 324 | */ 325 | public function ___render($options = array()) { 326 | return $this->renderInstance($options); 327 | } 328 | 329 | /** 330 | * Render further instance 331 | * available keys: 332 | * allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText 333 | * emailMessage, emailAddMessage, successMessage, errorMessage 334 | * sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo 335 | * saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup 336 | * 337 | * @param array $options 338 | * @return string 339 | */ 340 | private function renderInstance($options) { 341 | // overwrite module config settings 342 | foreach ($options as $key => $value) { 343 | switch ($key) { 344 | // === fields 345 | case 'allFields': 346 | $this->allFields = explode(',', $value); 347 | $allFieldsExtended = explode(',', $value); 348 | foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f; 349 | $this->allFieldsExtended = $allFieldsExtended; 350 | break; 351 | 352 | case 'redirectSamePage': 353 | $this->redirectSamePage = $value; 354 | break; 355 | 356 | case 'redirectPage': 357 | $this->redirectPage = $value; 358 | break; 359 | 360 | // === names 361 | case 'submitName': 362 | $this->submitName = $value; 363 | break; 364 | 365 | case 'btnClass': 366 | $this->btnClass = $value; 367 | break; 368 | 369 | case 'btnText': 370 | $this->btnText = $value; 371 | break; 372 | 373 | // === messages 374 | case 'successMessage': 375 | $this->successMessage = $value; 376 | break; 377 | case 'errorMessage': 378 | $this->errorMessage = $value; 379 | break; 380 | case 'emailMessage': 381 | $this->emailMessage = $value; 382 | break; 383 | case 'emailAddMessage': 384 | $this->emailAddMessage = $value; 385 | break; 386 | 387 | // === email 388 | case 'sendEmails': 389 | $this->sendEmails = $value; 390 | break; 391 | case 'emailSubject': 392 | $this->emailSubject = $value; 393 | break; 394 | case 'emailTo': 395 | $this->emailTo = $value; 396 | break; 397 | case 'emailServer': 398 | $this->emailServer = $value; 399 | break; 400 | case 'emailReplyTo': 401 | $this->emailReplyTo = $value; 402 | break; 403 | case 'emailAdd': 404 | $this->emailAdd = $value; 405 | break; 406 | case 'emailAddSubject': 407 | $this->emailAddSubject = $value; 408 | break; 409 | case 'emailAddTo': 410 | $this->emailAddTo = $value; 411 | break; 412 | case 'emailAddReplyTo': 413 | $this->emailAddReplyTo = $value; 414 | break; 415 | 416 | // === general 417 | case 'saveMessages': 418 | $this->saveMessages = $this->boolval($value); 419 | break; 420 | 421 | case 'saveMessagesParent': 422 | $this->saveMessagesParent = $value; 423 | break; 424 | 425 | case 'saveMessagesTemplate': 426 | $this->saveMessagesTemplate = $value; 427 | break; 428 | } 429 | } 430 | 431 | // send additional email? 432 | if (isset($this->emailAdd) && $this->emailAdd) { 433 | if (!isset($this->emailAddMessage)) $this->emailAddMessage = $this->emailMessage; 434 | if (!isset($this->emailAddTo)) $this->emailAddTo = $this->emailTo; 435 | if (!isset($this->emailAddReplyTo)) $this->emailAddReplyTo = $this->emailReplyTo; 436 | if (!isset($this->emailAddSubject)) $this->emailAddSubject = $this->emailSubject; 437 | } 438 | 439 | // execute render 440 | return $this->renderForm($options); 441 | } 442 | 443 | /** 444 | * Get mail message content 445 | * 446 | * @param string $text 447 | * @return string 448 | */ 449 | private function getMessageContent($text) { 450 | if (!empty($text)) { 451 | $date = new \DateTime(); 452 | if (preg_match('/\%date\%/', $text)) $text = str_replace('%date%', $date->format('Y-m-d H:i:s'), $text); 453 | preg_match_all('/\%(.*?)\%/', $text, $matches); 454 | 455 | foreach ($matches[0] as $key => $match) { 456 | $text = str_replace($match, $this->sanitizer->textarea($this->input->post->{$matches[1][$key]}), $text); 457 | } 458 | } else { 459 | $message = array(); 460 | foreach ($this->allFields as $inputfield) { 461 | $message[] = $inputfield . ': ' . $this->sanitizer->textarea($this->input->post->{$inputfield}); 462 | } 463 | $date = new \DateTime(); 464 | $message[] = 'Date: ' . $date->format('Y-m-d H:i:s'); 465 | $text = implode("\r\n", $message); 466 | } 467 | 468 | return $text; 469 | } 470 | 471 | /** 472 | * Send Mail 473 | */ 474 | public function ___sendMail() { 475 | $mail = new Mailer( 476 | $this->emailTo, 477 | $this->emailServer, 478 | $this->emailReplyTo, 479 | $this->emailSubject, 480 | trim($this->getMessageContent($this->emailMessage)) 481 | ); 482 | 483 | $numSent = $mail->send(); 484 | 485 | // send additional mail 486 | if ($this->sendEmails && isset($this->emailAdd) && $this->emailAdd) $this->sendAdditionalMail(); 487 | 488 | // log whether a mail has been sent or not 489 | if ($numSent) { 490 | $logmessage = array( 491 | $_SERVER['HTTP_USER_AGENT'], 492 | $_SERVER['REMOTE_ADDR'], 493 | $this->emailTo 494 | ); 495 | 496 | $this->log->save('[SUCCESS] ' . implode(', ', $logmessage)); 497 | } else { 498 | // mail has not been sent 499 | $this->log->save("[ERROR] Mail has not been sent to {$this->emailTo}"); 500 | } 501 | } 502 | 503 | /** 504 | * Save Message 505 | */ 506 | public function ___saveMessage() { 507 | $date = new \DateTime(); 508 | 509 | $name = array($date->getTimestamp()); 510 | if ($parts = $this->saveMessagesScheme) { 511 | foreach ($parts as $part) { 512 | $name[] = $this->input->post->$part; 513 | } 514 | } 515 | $pageName = implode(' ', $name); 516 | 517 | $p = new Page(); 518 | $p->template = $this->saveMessagesTemplate; 519 | $p->parent = wire('pages')->get($this->saveMessagesParent); 520 | $p->name = $this->sanitizer->pageName($pageName, true); 521 | $p->title = $pageName; 522 | $p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below! 523 | 524 | foreach ($this->allFields as $in) $p->$in = $this->input->post->$in; 525 | $p->scf_date = $date->getTimestamp(); 526 | $p->scf_ip = $_SERVER['REMOTE_ADDR']; 527 | $p->save(); 528 | } 529 | 530 | /** 531 | * Send Additional Mail 532 | */ 533 | public function ___sendAdditionalMail() { 534 | $mail = new Mailer( 535 | $this->emailAddTo, 536 | $this->emailServer, 537 | $this->emailAddReplyTo, 538 | $this->emailAddSubject, 539 | trim($this->getMessageContent($this->emailAddMessage)) 540 | ); 541 | 542 | if ($mail->send()) { 543 | $logmessage = array( 544 | $_SERVER['HTTP_USER_AGENT'], 545 | $_SERVER['REMOTE_ADDR'], 546 | $this->emailAddTo 547 | ); 548 | 549 | $this->log->save('[SUCCESS] ' . implode(', ', $logmessage)); 550 | } else { 551 | $this->log->save("[ERROR] Additional mail could not be sent to {$this->emailAddTo}"); 552 | } 553 | } 554 | 555 | /** 556 | * Hook create and add template fields 557 | * 558 | * @param HookEvent $event 559 | */ 560 | public function createAndAddFieldsOnSaveModuleConfigData(HookEvent $event) { 561 | if ($event->arguments[0] === self::CLASS_NAME) { 562 | $configData = $event->arguments[1]; 563 | 564 | // saveMessages enabled? create template if it doesn't exist 565 | $fg = $configData['saveMessages'] ? $this->createSaveMessagesTemplate($configData) : null; 566 | 567 | // get fields 568 | if ($configData['addFields']) { 569 | $this->addNewFields($configData, $fg); 570 | $event->setArgument(1, $configData); 571 | } 572 | 573 | // cleanup, update from 0.x to 1.x @todo: deprecated 574 | $this->upgradeItems($fg, $configData); 575 | } 576 | } 577 | 578 | /** 579 | * Add new fields 580 | * 581 | * @param array $configData 582 | * @param Fieldgroup $fg 583 | */ 584 | protected function addNewFields(&$configData, $fg) { 585 | $newFields = $configData['addFields']; 586 | $allFields = $configData['allFields']; 587 | 588 | foreach (explode(',', preg_replace('/\s/', '', $newFields)) as $name) { 589 | if (is_null($this->fields->get("scf_$name"))) { 590 | $f = new Field(); 591 | $f->type = $this->modules->get('FieldtypeText'); 592 | $f->name = "scf_$name"; 593 | $f->label = 'SCF - ' . ucfirst($name); 594 | $f->tags = self::TAG_NAME; 595 | $f->columnWidth = '25'; 596 | $f->save(); 597 | 598 | // saveMessages enabled - save fields to template 599 | if ($fg) { 600 | $fg->add($f); // add field to fieldgroup 601 | $fg->save(); // save fieldgroup 602 | } 603 | } 604 | 605 | if (!in_array("scf_$name", $allFields)) $allFields[] = "scf_$name"; 606 | } 607 | 608 | $configData['allFields'] = $allFields; 609 | $configData['addFields'] = ''; 610 | } 611 | 612 | /** 613 | * Create save messages template 614 | * 615 | * @param array $configData 616 | * @return Fieldgroup 617 | */ 618 | protected function createSaveMessagesTemplate($configData) { 619 | if ($template = $this->templates->get(self::SM_TEMPLATE_NAME)) { 620 | $fg = $template->fieldgroup; // get existing fieldgroup 621 | } else { 622 | // new fieldgroup 623 | $fg = new Fieldgroup(); 624 | $fg->name = self::SM_TEMPLATE_NAME; 625 | $fg->add($this->fields->get('title')); // needed title field 626 | $fg->save(); 627 | 628 | // new template 629 | $template = new Template(); 630 | $template->name = self::SM_TEMPLATE_NAME; 631 | $template->fieldgroup = $fg; // add the fieldgroup 632 | $template->slashUrls = 1; 633 | $template->noPrependTemplateFile = 1; 634 | $template->noAppendTemplateFile = 1; 635 | $template->tags = self::TAG_NAME; 636 | $template->save(); 637 | } 638 | 639 | // scf_spamip (check) 640 | if (!$fg->scf_spamip) { 641 | if (!$field = $this->fields->get('scf_spamIp')) { 642 | $field = new Field(); 643 | $field->type = $this->modules->get('FieldtypeCheckbox'); 644 | $field->name = 'scf_spamIp'; 645 | $field->value = 1; 646 | } 647 | 648 | $field->label = __('Add IP to spam list'); 649 | $field->description = __('If you activate this checkbox, further contact requests from this ip address will be treated as spam.'); 650 | $field->columnWidth = 25; 651 | $field->save(); 652 | 653 | $fg->add($field); 654 | $fg->save(); 655 | } 656 | 657 | // scf_date (datetime) 658 | if (!$fg->scf_date) { 659 | if (!$field = $this->fields->get('scf_date')) { 660 | $field = new Field(); 661 | $field->name = 'scf_date'; 662 | } 663 | 664 | $field->type = $this->modules->get('FieldtypeDatetime'); 665 | $field->label = __('Creation date'); 666 | $field->datepicker = InputfieldDatetime::datepickerClick; 667 | $field->dateInputFormat = 'Y/m/d'; 668 | $field->timeInputFormat = 'H:i'; 669 | $field->dateOutputFormat = 'Y/m/d'; 670 | $field->timeOutputFormat = 'H:i'; 671 | $field->columnWidth = 25; 672 | $field->save(); 673 | 674 | $fg->add($field); 675 | $fg->save(); 676 | } 677 | 678 | // scf_ip (text) 679 | if (!$fg->scf_ip) { 680 | if (!$field = $this->fields->get('scf_ip')) { 681 | $field = new Field(); 682 | $field->type = $this->modules->get('FieldtypeText'); 683 | $field->name = 'scf_ip'; 684 | } 685 | 686 | $field->label = __('IP address'); 687 | $field->columnWidth = 25; 688 | $field->save(); 689 | 690 | $fg->add($field); 691 | $fg->save(); 692 | } 693 | 694 | $this->validateParent($configData); 695 | return $fg; 696 | } 697 | 698 | /** 699 | * Validate parent page 700 | * 701 | * @param array $configData 702 | */ 703 | protected function validateParent($configData) { 704 | // add default for save messages parent 705 | if (!$configData['saveMessagesParent']) $configData['saveMessagesParent'] = $this->config->rootPageID; 706 | 707 | // check whether selected parent allows children (noChildren must be 0) 708 | if ($this->pages->get($configData['saveMessagesParent'])->template->noChildren > 0) { 709 | $this->log->error($this->_('Please choose another parent or change the belonging template. It must allow children.')); 710 | } 711 | } 712 | 713 | /** 714 | * Validate parent page 715 | * 716 | * @param Fieldgroup $fg 717 | * @param array $configData 718 | */ 719 | protected function upgradeItems($fg, $configData) { 720 | if ($this->fields->get('repeater_scfmessages') && $configData['saveMessages']) { 721 | // first: add fields to template 722 | foreach ($this->allFields as $f) { 723 | $fg->add($f); // add field to fieldgroup 724 | $fg->save(); // save fieldgroup 725 | } 726 | 727 | // second: repater items to pages 728 | foreach ($this->pages->get('template=' . self::SM_TEMPLATE_NAME)->repeater_scfmessages as $item) { 729 | if (!$item->scf_date) continue; 730 | 731 | $name = array($item->created); 732 | if ($parts = $configData['saveMessagesScheme']) { 733 | foreach ($parts as $part) { 734 | $name[] = $item->$part; 735 | } 736 | } 737 | $pageName = implode(' ', $name); 738 | 739 | if ($this->pages->find('name=' . $this->sanitizer->pageName($pageName, true))->count() > 0) continue; 740 | 741 | $p = new Page(); 742 | $p->template = self::SM_TEMPLATE_NAME; 743 | $p->parent = wire('pages')->get($configData['saveMessagesParent']); 744 | $p->name = $this->sanitizer->pageName($pageName, true); 745 | $p->title = $pageName; 746 | $p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below! 747 | 748 | $date = new \DateTime(); 749 | $date->setTimestamp($item->created); 750 | $p->scf_date = $date->format('Y/m/d H:i'); 751 | $p->scf_ip = $item->scf_ip; 752 | foreach ($this->allFields as $f) if ($item->$f) $p->$f = $item->$f; 753 | $p->save(); 754 | } 755 | 756 | // third: delete repeater and template simple_contact_form 757 | $fg->remove($this->fields->get('repeater_scfmessages')); 758 | $fg->save(); 759 | $this->fields->delete($this->fields->get('repeater_scfmessages')); 760 | $this->templates->delete($this->templates->get(self::TEMPLATE_NAME)); 761 | } 762 | } 763 | 764 | /** 765 | * Get the boolean value of a variable 766 | * 767 | * @param $val 768 | */ 769 | public function boolval($val) { 770 | if (!function_exists('boolval')) { 771 | // (PHP 5 < 5.5.0) 772 | $bool = (bool) $val; 773 | } else { 774 | // (PHP 5 >= 5.5.0) 775 | $bool = boolval($val); 776 | } 777 | 778 | return $bool; 779 | } 780 | 781 | /** 782 | * Hookable method called after the form was processed 783 | * Allows custom/extra validation and field manipulation 784 | */ 785 | protected function ___processValidation($form) {} 786 | 787 | } 788 | -------------------------------------------------------------------------------- /SimpleContactFormConfig.php: -------------------------------------------------------------------------------- 1 | templates->get('simple_contact_form_messages'); 13 | 14 | return array( 15 | 'sendEmails' => true, 16 | 'emailTo' => '', 17 | 'emailSubject' => 'New Web Contact Form Submission', 18 | 'emailMessage' => '', 19 | 'emailServer' => 'noreply@server.com', 20 | 'emailReplyTo' => '', 21 | 'allFields' => array(), 22 | 'redirectPage' => '', 23 | 'redirectSamePage' => true, 24 | 'saveMessages' => false, 25 | 'saveMessagesParent' => false, 26 | 'saveMessagesTemplate' => $saveMessagesTemplate ? $saveMessagesTemplate->id : null, 27 | 'saveMessagesScheme' => '', 28 | 'antiSpamTimeMin' => '1', 29 | 'antiSpamTimeMax' => '300', 30 | 'antiSpamPerDay' => '3', 31 | 'antiSpamExcludeIps' => '127.0.0.1', 32 | 'cleanup' => 0 33 | ); 34 | } 35 | 36 | /** 37 | * Retrieves the list of config input fields 38 | * Implementation of the ConfigurableModule interface 39 | * 40 | * @return InputfieldWrapper 41 | */ 42 | public function getInputfields() { 43 | $allFields = isset($this->data['allFields']) ? $this->data['allFields'] : array(); 44 | if (!is_array($allFields)) $allFields = explode(',', $this->data['allFields']); // @todo: deprecated 45 | 46 | // add prefix if necessary 47 | // @todo: deprecated 48 | foreach ($allFields as $key => $f) { 49 | if (!$this->fields->get($f) || $f === 'email') $allFields[$key] = 'scf_' . $f; 50 | } 51 | 52 | // get inputfields 53 | $inputfields = parent::getInputfields(); 54 | 55 | // fieldset general 56 | $fieldset = $this->modules->get('InputfieldFieldset'); 57 | $fieldset->label = __('Email'); 58 | 59 | // field send emails 60 | $field = $this->modules->get('InputfieldCheckbox'); 61 | $field->name = 'sendEmails'; 62 | $field->label = __('Send Emails?'); 63 | $field->description = __('Should Emails be sent?'); 64 | $field->value = 1; 65 | $field->columnWidth = 50; 66 | $fieldset->add($field); 67 | 68 | // field email subject 69 | $field = $this->modules->get('InputfieldText'); 70 | $field->name = 'emailSubject'; 71 | $field->label = __('Email: Subject'); 72 | $field->columnWidth = 50; 73 | $field->required = 1; 74 | $field->requiredIf = 'sendEmails=1'; 75 | $field->showIf = 'sendEmails=1'; 76 | $fieldset->add($field); 77 | 78 | // field email to 79 | $field = $this->modules->get('InputfieldText'); 80 | $field->name = 'emailTo'; 81 | $field->label = __('Email: "To"-Address'); 82 | $field->notes = __('Scheme: "Example{out}
" 14 | ), 15 | 'classes' => array( 16 | 'form' => 'form form__whatever', 17 | 'list' => 'list-item' 18 | ) 19 | ); 20 | 21 | echo $scf->render($options); 22 | ``` 23 | 24 | If you need additional markup before or after the form, you can use `prependMarkup` and `appendMarkup`. 25 | 26 | Below is the list of all available customization options copied from [ProcessWire master][1]. 27 | 28 | ```php 29 | /** 30 | * Markup used during the render() method 31 | * 32 | */ 33 | static protected $defaultMarkup = array( 34 | 'list' => "{out}\n", 35 | 'item' => "\n\t{out}
", 41 | 'item_head' => "\n{out}
", 43 | 'item_icon' => "", 44 | 'item_toggle' => "", 45 | // ALSO: 46 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 47 | ); 48 | 49 | /** 50 | * Classes used during the render() method 51 | * 52 | */ 53 | static protected $defaultClasses = array( 54 | 'form' => 'form js-simplecontactform', // additional clases for inputfieldform (optional) 55 | 'form_error' => 'form--error--message', 56 | 'form_success' => 'form--success--message', 57 | 'list' => 'fields', 58 | 'list_clearfix' => 'clearfix', 59 | 'item' => 'form__item form__item--{name}', 60 | 'item_label' => '', // additional classes for inputfieldheader (optional) 61 | 'item_content' => '', // additional classes for inputfieldcontent (optional) 62 | 'item_required' => 'field--required', // class is for inputfield 63 | 'item_error' => 'field--error', // note: not the same as markup[item_error], class is for inputfield 64 | 'item_collapsed' => 'field--collapsed', 65 | 'item_column_width' => 'field__column', 66 | 'item_column_width_first' => 'field__column--first', 67 | 'item_show_if' => 'field--show-if', 68 | 'item_required_if' => 'field--required-if' 69 | // ALSO: 70 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 71 | ); 72 | ``` 73 | 74 | ## Trouble Shooting 75 | 76 | Normally you're able to override the markup on a per-Intputfield basis like mentioned above: 77 | 78 | ```php 79 | 'markup' => array( 80 | // @see: https://github.com/processwire/ProcessWire/blob/master/wire/core/InputfieldWrapper.php#L44 81 | 'InputfieldSubmit' => array( 82 | // any of the properties above to override on a per-Inputifeld basis 83 | ) 84 | ), 85 | ``` 86 | 87 | Example: 88 | 89 | ```php 90 | $scf = $modules->get('SimpleContactForm'); 91 | 92 | $options = array( 93 | 'btnClass' => 'btn btn-blue btn-effect', 94 | 'btnText' => 'Send', 95 | 'classes' => array( 96 | 'item' => 'input-field' 97 | ) 98 | ); 99 | 100 | $content .= $scf->render($options); 101 | ``` 102 | 103 | However this doesn't seem to work in some cases (using InputfielSubmit or InputfieldButton). 104 | But you can override the `render` function of the specific class, in this example `InputfieldSubmit` (for example in `init.php`): 105 | 106 | ```php 107 | $this->addHook('InputfieldSubmit::render', function(HookEvent $event) { 108 | if ($this->page->template->name === 'contact') { // adapt template name to compare with 109 | $parent = (object)$event->object; 110 | $attrs = $parent->getAttributesString(); 111 | $value = $parent->entityEncode($parent->attr('value')); 112 | $out = ""; 113 | $event->return = $out; 114 | } 115 | }); 116 | ``` 117 | 118 | One more example: 119 | 120 | ```php 121 | $this->addHookBefore('Inputfield::render', function(HookEvent $event) { 122 | if ($this->page->template->name === 'contact') { // adapt template name to compare with 123 | $inputfield = $event->object; 124 | $inputfield->addClass('col-sm-8'); 125 | $event->return = $inputfield; 126 | } 127 | }); 128 | ``` 129 | 130 | [1]: https://github.com/processwire/ProcessWire/blob/master/wire/core/InputfieldWrapper.php#L44 'ProcessWire master' 131 | -------------------------------------------------------------------------------- /doc/spam-translate.md: -------------------------------------------------------------------------------- 1 | # How to translate the spam message 2 | 3 | You could translate the values – which will be inserted – with 'spam' and 'time'. 4 | 5 | Or, you could also translate *"Sorry, but your message didn't pass our %s test. Please try another %s."* 6 | with *"Sorry, but your message didn't pass our spam test. Please try another time."*. 7 | -------------------------------------------------------------------------------- /doc/success-message.md: -------------------------------------------------------------------------------- 1 | # How to add a custom success message 2 | 3 | ## Option 1 4 | 5 | Simple set it as option parameter: 6 | 7 | ``` php 8 | $modules->get('SimpleContactForm')->render(array( 9 | 'emailMessage' => 'Custom Message' 10 | )); 11 | ``` 12 | 13 | ## Option 2 14 | 15 | Make it translatable: 16 | 17 | ``` php 18 | $modules->get('SimpleContactForm')->render(array( 19 | 'emailMessage' => __('Custom Message') 20 | )); 21 | ``` 22 | 23 | ## Option 3 24 | 25 | Set it as variable before: 26 | 27 | ``` php 28 | $emailMessage = $page->title . ' test'; 29 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 30 | ``` 31 | 32 | ## Option 4 33 | 34 | Include the content from another file: 35 | 36 | ``` php 37 | include('./message.php'); 38 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 39 | 40 | // message.php 41 | title . ' some content ' . $input->post->fieldname; 44 | ``` 45 | 46 | ## Option 5 47 | 48 | Include the content from another file using output buffering: 49 | 50 | ``` php 51 | ob_start(); 52 | include('./message.php'); 53 | $emailMessage = ob_get_clean(); 54 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 55 | 56 | // message.php 57 | title . ' test3 ' . $input->post->headline; 59 | ``` 60 | 61 | ## Using twig 62 | 63 | ```html 64 | {% set emailMessage %}{% include 'mails/recommend.twig' %}{% endset %} 65 | {% set options = { 66 | 'action': './#recommend', 67 | 'emailMessage': emailMessage 68 | } %} 69 | {{modules.get('SimpleContactForm').render(options)}} 70 | 71 | {# recommend.twig #} 72 | ... 73 | {{ estate.title }} 74 | {{ page.httpUrl }} 75 | {% if input.scf_salutation == 1 %}xx{% elseif input.scf_salutation == 2 %}xx{% else %}xx{% endif %} 76 | ... 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/Mailer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2017 8 | * @filesource 9 | */ 10 | 11 | /** 12 | * Class Mailer 13 | */ 14 | class Mailer extends \ProcessWire\Wire { 15 | 16 | /** 17 | * construct 18 | * 19 | * @param string $to 20 | * @param string $from 21 | * @param string $replyTo 22 | * @param string $subject 23 | * @param string $body 24 | */ 25 | public function __construct($to, $from, $replyTo, $subject, $body) { 26 | $this->subject = $subject; 27 | $this->body = $body; 28 | 29 | $this->to = array(); 30 | foreach (explode(',', $to) as $value) { 31 | list($toEmail, $toName) = $this->extractEmailAndName($value); 32 | $this->to[] = "$toName <$toEmail>"; 33 | } 34 | 35 | list($fromEmail, $fromName) = $this->extractEmailAndName($from); 36 | $this->from = "$fromName <$fromEmail>"; 37 | 38 | if ($replyTo) { 39 | list($replyToEmail, $replyToName) = $this->extractEmailAndName($replyTo); 40 | $this->replyTo = "$replyToName <$replyToEmail>"; 41 | } else { 42 | $this->replyTo = ''; 43 | } 44 | } 45 | 46 | /** 47 | * extract email from name 48 | * substitute umlaute 49 | * 50 | * @param string $email 51 | * @return array 52 | */ 53 | protected function extractEmailAndName($email) { 54 | $name = ''; 55 | if (strpos($email, '<') !== false && strpos($email, '>') !== false) { 56 | // email has separate from name and email 57 | if (preg_match('/^(.*?)<([^>]+)>.*$/', $email, $matches)) { 58 | $name = preg_replace( 59 | array('/ä/', '/ö/', '/ü/', '/Ä/', '/Ö/', '/Ü/','/ß/'), 60 | array('ae', 'oe', 'ue', 'Ae', 'Oe', 'Ue', 'ss'), 61 | $matches[1] 62 | ); 63 | $email = $matches[2]; 64 | } 65 | } 66 | 67 | return array($email, $name); 68 | } 69 | 70 | /** 71 | * send mail 72 | * 73 | * @return boolean 74 | */ 75 | public function send() { 76 | $wireMail = \ProcessWire\wireMail(); // don't use `new WireMail()` which bypasses WireMailSMTP 77 | 78 | $wireMail->to($this->to); 79 | $wireMail->from($this->from); 80 | $wireMail->subject($this->subject); 81 | $wireMail->body($this->body); 82 | 83 | if ($this->replyTo) $wireMail->header('Reply-To', $this->replyTo); 84 | 85 | return $wireMail->send(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /lib/SpamProtection.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2017 11 | * @filesource 12 | */ 13 | 14 | /** 15 | * Class SpamProtection 16 | */ 17 | class SpamProtection extends \ProcessWire\Wire { 18 | 19 | /** 20 | * User agents marked as spam 21 | */ 22 | const USER_AGENTS = '#w3c|google|slurp|msn|yahoo|y!j|altavista|ask|spider|search|bot|crawl|usw#i'; 23 | 24 | /** 25 | * boolean isSpam 26 | */ 27 | protected static $isSpam = false; 28 | 29 | /** 30 | * Error messages used in log file 31 | */ 32 | protected static $errorMessages = array( 33 | 'default' => 'An error occured.', 34 | 'token' => 'CSRF Token validation failed.', 35 | 'honeypot' => 'Honeypot field was filled.', 36 | 'numberOfFields' => 'Number of fields does not match.', 37 | 'userAgent' => 'User Agent is not allowed.', 38 | 'httpParams' => 'User Agent and HTTP Referer are empty.', 39 | 'timeRange' => 'Date difference is out of range.', 40 | 'ipAddress' => 'This IP address was already marked as spam.', 41 | 'numberOfSubmits' => 'This IP address submitted this form too often.' 42 | ); 43 | 44 | /** 45 | * Is valid checks 46 | * Not depending on whether messages should be saved 47 | */ 48 | protected static $isValidChecks = array( 49 | 'validToken', 50 | 'validHoneypot', 51 | 'validNumberOfFields', 52 | 'validUserAgent', 53 | 'validHttpParams', 54 | 'validTimeRange' 55 | ); 56 | 57 | /** 58 | * Is valid save messages checks 59 | * Depending on whether messages should be saved 60 | */ 61 | protected static $isValidSMChecks = array( 62 | 'validIpAddress', 63 | 'validNumberOfSubmits' 64 | ); 65 | 66 | /** 67 | * Construct 68 | */ 69 | public function __construct() { 70 | $this->setLogFile(); 71 | $this->currentIp = $_SERVER['REMOTE_ADDR']; 72 | } 73 | 74 | /** 75 | * Set log file 76 | */ 77 | public function setLogFile() { 78 | $this->scfLog = strtolower(SCF::CLASS_NAME . '-log'); 79 | } 80 | 81 | /** 82 | * Set number of inputs to compare with 83 | * 84 | * @param integer $count 85 | * @return SpamProtection 86 | */ 87 | public function setCount($count) { 88 | $this->count = (int)$count; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set time range a submission is valid 94 | * 95 | * @param integer $min 96 | * @param integer $max 97 | * @return SpamProtection 98 | */ 99 | public function setTimeRange($min, $max) { 100 | $this->timeRange = (object)array( 101 | 'min' => $min, 102 | 'max' => $max 103 | ); 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set whether messages should be saved 109 | * 110 | * @param boolean $saveMessages 111 | * @return SpamProtection 112 | */ 113 | public function setSaveMessages($saveMessages) { 114 | $this->saveMessages = $saveMessages; 115 | return $this; 116 | } 117 | 118 | /** 119 | * Set ip addresses which should be excluded 120 | * 121 | * @param string $excludeIpAdresses 122 | * @return SpamProtection 123 | */ 124 | public function setExcludeIpAdresses($excludeIpAdresses) { 125 | $this->excludeIpAdresses = explode(',', $excludeIpAdresses); 126 | return $this; 127 | } 128 | 129 | /** 130 | * Set maximum number of submits per day 131 | * 132 | * @param string $numberOfSubmitsPerDay 133 | * @return SpamProtection 134 | */ 135 | public function setNumberOfSubmitsPerDay($numberOfSubmitsPerDay) { 136 | $this->numberOfSubmitsPerDay = $numberOfSubmitsPerDay; 137 | return $this; 138 | } 139 | 140 | /** 141 | * Whether spam was detected 142 | * 143 | * @return boolean 144 | */ 145 | public function isSpam() { 146 | return self::$isSpam; 147 | } 148 | 149 | /** 150 | * Get random animal to build error message 151 | * 152 | * @return string 153 | */ 154 | public function getAnimal() { 155 | $animals = array( 156 | $this->_('monkey'), 157 | $this->_('squirrel'), 158 | $this->_('giraffe'), 159 | $this->_('marmot') 160 | ); 161 | 162 | return $animals[array_rand($animals)]; 163 | } 164 | 165 | /** 166 | * Get random fruit to build error message 167 | * 168 | * @return string 169 | */ 170 | public function getFruit() { 171 | $fruits = array( 172 | $this->_('strawberry'), 173 | $this->_('banana'), 174 | $this->_('peanut'), 175 | $this->_('blueberry') 176 | ); 177 | 178 | return $fruits[array_rand($fruits)]; 179 | } 180 | 181 | /** 182 | * Set whether the request is marked as spam 183 | * 184 | * @param boolean $isSpam 185 | */ 186 | protected function setIsSpam($isSpam = true) { 187 | self::$isSpam = $isSpam; 188 | } 189 | 190 | /** 191 | * Add log entry 192 | * 193 | * @param string $key 194 | */ 195 | protected function addLogEntry($key) { 196 | $this->log->save($this->scfLog, "[FAILURE] {$this->getErrorMessage($key)} IP: {$this->currentIp}"); 197 | } 198 | 199 | /** 200 | * Get specific error message 201 | * 202 | * @param string $key 203 | * @return string 204 | */ 205 | protected function getErrorMessage($key) { 206 | if (!array_key_exists($key, self::$errorMessages)) $key = 'default'; 207 | return self::$errorMessages[$key]; 208 | } 209 | 210 | /** 211 | * Check CSRF token 212 | */ 213 | protected function validToken() { 214 | try { 215 | $this->session->CSRF->validate(); 216 | } catch (WireCSRFException $e) { 217 | $this->setIsSpam(); 218 | $this->addLogEntry('token'); 219 | } 220 | } 221 | 222 | /** 223 | * Check if the honeypot field was filled 224 | */ 225 | protected function validHoneypot() { 226 | if ($this->input->post->{'scf-website'}) { 227 | $this->setIsSpam(); 228 | $this->addLogEntry('honeypot'); 229 | } 230 | } 231 | 232 | /** 233 | * Check if the number of fields match 234 | */ 235 | protected function validNumberOfFields() { 236 | if (count($this->input->post) !== $this->count) { 237 | $this->setIsSpam(); 238 | $this->addLogEntry('numberOfFields'); 239 | } 240 | } 241 | 242 | /** 243 | * Check the user agent 244 | */ 245 | protected function validUserAgent() { 246 | if (preg_match(self::USER_AGENTS, $_SERVER['HTTP_USER_AGENT'])) { 247 | $this->setIsSpam(); 248 | $this->addLogEntry('userAgent'); 249 | } 250 | } 251 | 252 | /** 253 | * Check http referrer and user agent 254 | */ 255 | protected function validHttpParams() { 256 | if ($_SERVER['HTTP_REFERER'] === '' && $_SERVER['HTTP_USER_AGENT'] === '') { 257 | $this->setIsSpam(); 258 | $this->addLogEntry('httpParams'); 259 | } 260 | } 261 | 262 | /** 263 | * Check whether the form was submitted within a certain time range 264 | */ 265 | protected function validTimeRange() { 266 | $date = (int)$this->input->post->{'scf-date'}; 267 | $dateDiff = $date ? time() - $date : 0; 268 | if ($dateDiff <= $this->timeRange->min || $dateDiff >= $this->timeRange->max) { 269 | $this->setIsSpam(); 270 | $this->addLogEntry('timeRange'); 271 | } 272 | } 273 | 274 | /** 275 | * Check whether the ip address was marked as spam 276 | */ 277 | protected function validIpAddress() { 278 | $spamIpPages = $this->pages->find('template=' . SCF::SM_TEMPLATE_NAME . ', scf_spamIp!='); 279 | if (!$spamIpPages->count()) return; 280 | 281 | foreach ($spamIpPages as $spamIpPage) { 282 | if ($spamIpPage->scf_ip === $this->currentIp) { 283 | $this->setIsSpam(); 284 | $this->addLogEntry('ipAddress'); 285 | break; 286 | } 287 | } 288 | } 289 | 290 | /** 291 | * Check how often the form is allowed to be submitted by a single IP address 292 | */ 293 | protected function validNumberOfSubmits() { 294 | $dateSub = new \DateTime(); 295 | $dateSub->sub(new \DateInterval('P1D')); 296 | $selector = 'template=' . SCF::SM_TEMPLATE_NAME . ", scf_ip={$this->currentIp}, scf_date>={$dateSub->getTimestamp()}"; 297 | $totalLast24h = $this->pages->find($selector)->count(); 298 | 299 | if ($totalLast24h >= $this->numberOfSubmitsPerDay) { 300 | $this->setIsSpam(); 301 | $this->addLogEntry('numberOfSubmits'); 302 | } 303 | } 304 | 305 | /** 306 | * Validates the form 307 | * 308 | * @return SimpleContactForm 309 | */ 310 | public function validate() { 311 | foreach (self::$isValidChecks as $isValid) { 312 | $this->$isValid(); 313 | if (self::$isSpam) break; 314 | } 315 | 316 | // additional checks only if save messages feature is turned on 317 | if (!$this->isSpam() && $this->saveMessages) { 318 | if (!in_array($this->currentIp, $this->excludeIpAdresses)) { 319 | foreach (self::$isValidSMChecks as $isValid) { 320 | $this->$isValid(); 321 | if (self::$isSpam) break; 322 | } 323 | } 324 | } 325 | 326 | return $this; 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /resources/contact.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | const contact = () => { 4 | 5 | // set a global jquery function for submitting the form 6 | $.simplecontactform = () => { 7 | const $forms = $('.js-simplecontactform'); 8 | 9 | if ($forms.length) { 10 | // reinit the form-ajax submission 11 | $forms.off('submit.simplecontactform'); 12 | $forms.on('submit.simplecontactform', (e) => { 13 | const $form = $(e.currentTarget); 14 | 15 | e.preventDefault(); 16 | $.post(e.target.action, `${$form.serialize()}&submit=submit`, (data) => { 17 | $form.replaceWith($(data)); 18 | $.simplecontactform(); 19 | }); 20 | }); 21 | } 22 | }; 23 | 24 | // and run it once 25 | $.simplecontactform(); 26 | 27 | }; 28 | 29 | export default contact; 30 | -------------------------------------------------------------------------------- /resources/jquery.simplecontactform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Plugin 3 | * Author: Tabea David | tabea.david@kf-interactive.com 4 | */ 5 | 6 | ;(function($, window, document, undefined) { 7 | 8 | $.simplecontactform = function($element, options) { 9 | 10 | // some default vars and self reference for scope issues 11 | var plugin = {}; 12 | plugin.options = $.extend({}, $.simplecontactform.defaults, options); 13 | plugin.$forms = $element; 14 | 15 | // all plugin methods 16 | plugin = $.extend(plugin, { 17 | 18 | load: function() { 19 | if (plugin.$forms.length) { 20 | // reinit the form-ajax submission 21 | plugin.$forms.off('submit.simplecontactform'); 22 | plugin.$forms.on('submit.simplecontactform', function (e) { 23 | var $form = $(this); 24 | e.preventDefault(); 25 | 26 | $.post(e.target.action, $form.serialize() + '&submit=submit', function (data) { 27 | $form.parent().replaceWith($(data)); 28 | plugin.load(); 29 | }); 30 | }); 31 | } 32 | } 33 | 34 | 35 | }); 36 | 37 | // run the plugin 38 | // ====================================================================== 39 | plugin.load(); 40 | // ====================================================================== 41 | }; 42 | 43 | 44 | // define the plugin defaults here 45 | $.simplecontactform.defaults = { 46 | form: 'js-simplecontactform' 47 | }; 48 | 49 | 50 | // jquery wrapper function 51 | $.fn.simplecontactform = function(options) { 52 | return this.each(function() { 53 | var simplecontactform = $.simplecontactform(this, options); 54 | }); 55 | }; 56 | 57 | }(jQuery, window, document)); 58 | --------------------------------------------------------------------------------