Skip to content
26 changes: 21 additions & 5 deletions classes/BackendHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Martin\Forms\Classes;

use Backend;
use BackendAuth;
use Backend\Facades\Backend;
use Backend\Facades\BackendAuth;

class BackendHelpers
{
Expand Down Expand Up @@ -36,6 +36,22 @@ public static function isTranslatePlugin(): bool
return class_exists('\RainLab\Translate\Classes\Translator') && class_exists('\RainLab\Translate\Models\Message');
}

/**
* Recursively apply a callback function to every items in give array
*
* @param $callback
* @param $array
* @return array|false[]
*/
public static function array_map_recursive($callback, $array)
{
$func = function ($item) use (&$func, &$callback) {
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
};

return array_map($func, $array);
}

/**
* Render an array|object as HTML list (UL > LI)
*
Expand Down Expand Up @@ -76,9 +92,9 @@ public static function anonymizeIPv4(string $address): string
/**
* Extract string from curly braces
*
* @param string $pattern Pattern to replace
* @param string $replacement Replacement string
* @param string $subject Strings to replace
* @param string $pattern Pattern to replace
* @param string|null $replacement Replacement string
* @param string $subject Strings to replace
*
* @return string
*/
Expand Down
157 changes: 28 additions & 129 deletions classes/MagicForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@
abstract class MagicForm extends ComponentBase
{

use \Martin\Forms\Classes\ReCaptcha;
use \Martin\Forms\Classes\SharedProperties;
use \Martin\Forms\Classes\Traits\PostData;
use \Martin\Forms\Classes\Traits\ReCaptcha;
use \Martin\Forms\Classes\Traits\RequestValidation;
use \Martin\Forms\Classes\Traits\SendMails;
use \Martin\Forms\Classes\Traits\SharedProperties;

private $flash_partial;
private $validator;

public function init()
{
// FLASH PARTIAL
$this->flash_partial = $this->property('messages_partial', '@flash.htm');
}

public function onRun()
{
Expand Down Expand Up @@ -58,17 +70,8 @@ public function settings()

public function onFormSubmit()
{
// FLASH PARTIAL
$flash_partial = $this->property('messages_partial', '@flash.htm');

// CSRF CHECK
if (Config::get('cms.enableCsrfProtection') && (Session::token() != post('_token'))) {
throw new AjaxException(['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, [
'status' => 'error',
'type' => 'danger',
'content' => Lang::get('martin.forms::lang.components.shared.csrf_error'),
])]);
}
$this->checkCSRF();

// LOAD TRANSLATOR PLUGIN
if (BackendHelpers::isTranslatePlugin()) {
Expand All @@ -78,25 +81,8 @@ public function onFormSubmit()
\RainLab\Translate\Models\Message::setContext($locale);
}

// FILTER ALLOWED FIELDS
$allow = $this->property('allowed_fields');
if (is_array($allow) && !empty($allow)) {
foreach ($allow as $field) {
$post[$field] = post($field);
}
if ($this->isReCaptchaEnabled()) {
$post['g-recaptcha-response'] = post('g-recaptcha-response');
}
} else {
$post = post();
}

// SANITIZE FORM DATA
if ($this->property('sanitize_data') == 'htmlspecialchars') {
$post = $this->array_map_recursive(function ($value) {
return htmlspecialchars($value, ENT_QUOTES);
}, $post);
}
/** PREPARE FORM DATA */
$post = $this->preparePost();

// VALIDATION PARAMETERS
$rules = (array)$this->property('rules');
Expand All @@ -116,73 +102,24 @@ public function onFormSubmit()
}

// DO FORM VALIDATION
$validator = Validator::make($post, $rules, $msgs, $custom_attributes);
$this->validator = Validator::make($post, $rules, $msgs, $custom_attributes);

// NICE reCAPTCHA FIELD NAME
if ($this->isReCaptchaEnabled()) {
$fields_names = ['g-recaptcha-response' => 'reCAPTCHA'];
$validator->setAttributeNames(array_merge($fields_names, $custom_attributes));
}

// VALIDATE ALL + CAPTCHA EXISTS
if ($validator->fails()) {

// GET DEFAULT ERROR MESSAGE
$message = $this->property('messages_errors');

// LOOK FOR TRANSLATION
if (BackendHelpers::isTranslatePlugin()) {
$message = \RainLab\Translate\Models\Message::trans($message);
}

// THROW ERRORS
if ($this->property('inline_errors') == 'display') {
throw new ValidationException($validator);
} else {
throw new AjaxException($this->exceptionResponse($validator, [
'status' => 'error',
'type' => 'danger',
'title' => $message,
'list' => $validator->messages()->all(),
'errors' => json_encode($validator->messages()->messages()),
'jscript' => $this->property('js_on_error'),
]));
}
$this->validator->setAttributeNames(array_merge($fields_names, $custom_attributes));
}

// IF FIRST VALIDATION IS OK, VALIDATE CAPTCHA vs GOOGLE
// (this prevents to resolve captcha after every form error)
if ($this->isReCaptchaEnabled()) {
// CHECK FOR VALID FORM AND THROW ERROR IF NEEDED
$this->validateForm();

// PREPARE RECAPTCHA VALIDATION
$rules = ['g-recaptcha-response' => 'recaptcha'];
$err_msg = ['g-recaptcha-response.recaptcha' => Lang::get('martin.forms::lang.validation.recaptcha_error')];

// DO SECOND VALIDATION
$validator = Validator::make($post, $rules, $err_msg);

// VALIDATE ALL + CAPTCHA EXISTS
if ($validator->fails()) {

// THROW ERRORS
if ($this->property('inline_errors') == 'display') {
throw new ValidationException($validator);
} else {
throw new AjaxException($this->exceptionResponse($validator, [
'status' => 'error',
'type' => 'danger',
'content' => Lang::get('martin.forms::lang.validation.recaptcha_error'),
'errors' => json_encode($validator->messages()->messages()),
'jscript' => $this->property('js_on_error'),
]));
}
}
}
// IF FIRST VALIDATION IS OK, VALIDATE CAPTCHA vs GOOGLE (prevents to resolve captcha after every form error)
$this->validateReCaptcha($post);

// REMOVE EXTRA FIELDS FROM STORED DATA
unset($post['_token'], $post['g-recaptcha-response'], $post['_session_key'], $post['files']);

// FIRE BEFORE SAVE EVENT
/** FIRE BEFORE SAVE EVENT */
Event::fire('martin.forms.beforeSaveRecord', [&$post, $this]);

if (count($custom_attributes)) {
Expand All @@ -208,23 +145,10 @@ public function onFormSubmit()
$record->save(null, post('_session_key'));
}

// SEND NOTIFICATION EMAIL
if ($this->property('mail_enabled')) {
$notification = App::makeWith(Notification::class, [
$this->getProperties(), $post, $record, $record->files
]);
$notification->send();
}

// SEND AUTORESPONSE EMAIL
if ($this->property('mail_resp_enabled')) {
$autoresponse = App::makeWith(AutoResponse::class, [
$this->getProperties(), $post, $record
]);
$autoresponse->send();
}
/** SEND NOTIFICATION & AUTORESPONSE EMAILS */
$this->sendEmails($post, $record);

// FIRE AFTER SAVE EVENT
/** FIRE AFTER SAVE EVENT */
Event::fire('martin.forms.afterSaveRecord', [&$post, $this, $record]);

// CHECK FOR REDIRECT
Expand All @@ -241,30 +165,14 @@ public function onFormSubmit()
}

// DISPLAY SUCCESS MESSAGE
return ['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, [
return ['#' . $this->alias . '_forms_flash' => $this->renderPartial($this->flash_partial, [
'status' => 'success',
'type' => 'success',
'content' => $message,
'jscript' => $this->prepareJavaScript(),
])];
}

private function exceptionResponse($validator, $params)
{
// FLASH PARTIAL
$flash_partial = $this->property('messages_partial', '@flash.htm');

// EXCEPTION RESPONSE
$response = ['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, $params)];

// INCLUDE ERROR FIELDS IF REQUIRED
if ($this->property('inline_errors') != 'disabled') {
$response['error_fields'] = $validator->messages();
}

return $response;
}

private function prepareJavaScript()
{
$code = false;
Expand Down Expand Up @@ -303,15 +211,6 @@ private function getIP()
return $ip;
}

private function array_map_recursive($callback, $array)
{
$func = function ($item) use (&$func, &$callback) {
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
};

return array_map($func, $array);
}

private function attachFiles(Record $record)
{
$files = post('files', null);
Expand Down
3 changes: 1 addition & 2 deletions classes/ReCaptchaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

namespace Martin\Forms\Classes;

use Request;
use Martin\Forms\Models\Settings;
use Illuminate\Support\Facades\Request;

class ReCaptchaValidator
{

public function validateReCaptcha($attribute, $value, $parameters)
{
$secret_key = Settings::get('recaptcha_secret_key');
Expand Down
51 changes: 51 additions & 0 deletions classes/Traits/PostData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Martin\Forms\Classes\Traits;

use Martin\Forms\Classes\BackendHelpers;

trait PostData
{
/**
* Apply required transformations to form data
*
* @return array
*/
private function preparePost(): array
{
$allowed_fields = $this->property('allowed_fields');

if (empty($allowed_fields)) {
return input();
}

$post = [];

foreach ($allowed_fields as $field) {
$post[$field] = input($field);
}

if ($this->isReCaptchaEnabled()) {
$post['g-recaptcha-response'] = input('g-recaptcha-response');
}

if ($this->property('sanitize_data') == 'htmlspecialchars') {
$post = $this->sanitize($post);
}

return $post;
}

/**
* Sanitize form data using PHP "htmlspecialchars" function
*
* @param array $post
* @return array
*/
private function sanitize(array $post): array
{
return BackendHelpers::array_map_recursive(function ($value) {
return htmlspecialchars($value, ENT_QUOTES);
}, $post);
}
}
2 changes: 1 addition & 1 deletion classes/ReCaptcha.php → classes/Traits/ReCaptcha.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Martin\Forms\Classes;
namespace Martin\Forms\Classes\Traits;

use Martin\Forms\Classes\BackendHelpers;
use Martin\Forms\Models\Settings;
Expand Down
Loading