Form::buildForm()   F
last analyzed

Complexity

Conditions 36
Paths 98

Size

Total Lines 184
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1332

Importance

Changes 0
Metric Value
cc 36
eloc 101
nc 98
nop 0
dl 0
loc 184
ccs 0
cts 124
cp 0
crap 1332
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Frontend\Modules\FormBuilder\Widgets;
4
5
use Common\Exception\RedirectException;
6
use Frontend\Core\Engine\Base\Widget as FrontendBaseWidget;
7
use Frontend\Core\Engine\Form as FrontendForm;
8
use Frontend\Core\Language\Language as FL;
9
use Frontend\Core\Engine\Model as FrontendModel;
10
use Frontend\Core\Language\Locale;
11
use Frontend\Modules\FormBuilder\Engine\Model as FrontendFormBuilderModel;
12
use Frontend\Modules\FormBuilder\FormBuilderEvents;
13
use Frontend\Modules\FormBuilder\Event\FormBuilderSubmittedEvent;
14
use ReCaptcha\ReCaptcha;
15
use ReCaptcha\RequestMethod\CurlPost;
16
use SpoonFormAttributes;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
19
/**
20
 * This is the form widget.
21
 */
22
class Form extends FrontendBaseWidget
23
{
24
    /**
25
     * Fields in HTML form.
26
     *
27
     * @var array
28
     */
29
    private $fieldsHTML;
30
31
    /**
32
     * The form.
33
     *
34
     * @var FrontendForm
35
     */
36
    private $form;
37
38
    /**
39
     * Form name
40
     *
41
     * @var string
42
     */
43
    private $formName;
44
45
    /**
46
     * The form item.
47
     *
48
     * @var array
49
     */
50
    private $item;
51
52
    /**
53
     * @var bool
54
     */
55
    private $hasRecaptchaField;
56
57
    /**
58
     * Create form action and strip the identifier parameter.
59
     *
60
     * We use this function to create the action for the form.
61
     * This action cannot contain an identifier since these are used for
62
     * statistics and failed form submits cannot be tracked.
63
     *
64
     * @return string
65
     */
66
    private function createAction(): string
67
    {
68
        // pages
69
        $action = implode('/', $this->url->getPages());
70
71
        // init parameters
72
        $parameters = $this->url->getParameters();
73
        $moduleParameters = [];
74
        $getParameters = [];
75
76
        // sort by key (important for action order)
77
        ksort($parameters);
78
79
        // loop and filter parameters
80
        foreach ($parameters as $key => $value) {
81
            // skip identifier
82
            if ($key === 'identifier') {
83
                continue;
84
            }
85
86
            // normal parameter
87
            if (\SpoonFilter::isInteger($key)) {
88
                $moduleParameters[] = $value;
89
            } else {
90
                // get parameter
91
                $getParameters[$key] = $value;
92
            }
93
        }
94
95
        // single language
96
        if ($this->getContainer()->getParameter('site.multilanguage')) {
97
            $action = LANGUAGE . '/' . $action;
98
        }
99
100
        // add to action
101
        if (count($moduleParameters) > 0) {
102
            $action .= '/' . implode('/', $moduleParameters);
103
        }
104
        if (count($getParameters) > 0) {
105
            $action .= '?' . http_build_query($getParameters, null, '&', PHP_QUERY_RFC3986);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $numeric_prefix of http_build_query(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

105
            $action .= '?' . http_build_query($getParameters, /** @scrutinizer ignore-type */ null, '&', PHP_QUERY_RFC3986);
Loading history...
106
        }
107
108
        // remove trailing slash
109
        $action = rtrim($action, '/');
110
111
        // cough up action
112
        return '/' . $action;
113
    }
114
115
    public function execute(): void
116
    {
117
        parent::execute();
118
119
        $this->loadTemplate();
120
        $this->loadData();
121
122
        // success message
123
        if ($this->url->hasParameter('identifier')
124
            && $this->url->getParameter('identifier') === $this->item['identifier']
125
        ) {
126
            $this->parseSuccessMessage();
127
        } else {
128
            // create/handle form
129
            $this->buildForm();
130
            $this->validateForm();
131
            $this->parse();
132
        }
133
    }
134
135
    private function loadData(): void
136
    {
137
        // fetch the item
138
        $this->item = FrontendFormBuilderModel::get((int) $this->data['id']);
139
140
        // define form name
141
        $this->formName = 'form' . $this->item['id'];
142
    }
143
144
    private function buildForm(): void
145
    {
146
        // create form
147
        $this->form = new FrontendForm('form' . $this->item['id']);
148
149
        // exists and has fields
150
        if (!empty($this->item) && !empty($this->item['fields'])) {
151
            // loop fields
152
            foreach ($this->item['fields'] as $field) {
153
                // init
154
                $item = [
155
                    'name' => 'field' . $field['id'],
156
                    'type' => $field['type'],
157
                    'label' => $field['settings']['label'] ?? '',
158
                    'placeholder' => $field['settings']['placeholder'] ?? null,
159
                    'classname' => $field['settings']['classname'] ?? null,
160
                    'autocomplete' => $field['settings']['autocomplete'] ?? null,
161
                    'required' => isset($field['validations']['required']),
162
                    'validations' => $field['validations'] ?? [],
163
                    'html' => '',
164
                ];
165
166
                // form values
167
                $values = $field['settings']['values'] ?? null;
168
                $defaultValues = $field['settings']['default_values'] ?? null;
169
170
                if ($field['type'] === 'dropdown') {
171
                    // values and labels are the same
172
                    $values = array_combine($values, $values);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type null; however, parameter $values of array_combine() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
                    $values = array_combine($values, /** @scrutinizer ignore-type */ $values);
Loading history...
Bug introduced by
It seems like $values can also be of type null; however, parameter $keys of array_combine() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
                    $values = array_combine(/** @scrutinizer ignore-type */ $values, $values);
Loading history...
173
174
                    // get index of selected item
175
                    $defaultIndex = array_search($defaultValues, $values, true);
176
                    if ($defaultIndex === false) {
177
                        $defaultIndex = null;
178
                    }
179
180
                    // create element
181
                    $ddm = $this->form->addDropdown($item['name'], $values, $defaultIndex, false, $item['classname']);
182
183
                    // empty default element
184
                    $ddm->setDefaultElement('');
185
186
                    // add required attribute
187
                    if ($item['required']) {
188
                        $ddm->setAttribute('required', null);
189
                    }
190
191
                    $this->setCustomHTML5ErrorMessages($item, $ddm);
192
                    // get content
193
                    $item['html'] = $ddm->parse();
194
                } elseif ($field['type'] === 'radiobutton') {
195
                    // create element
196
                    $rbt = $this->form->addRadiobutton($item['name'], $values, $defaultValues, $item['classname']);
0 ignored issues
show
Bug introduced by
It seems like $values can also be of type null; however, parameter $values of Common\Core\Form::addRadiobutton() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
                    $rbt = $this->form->addRadiobutton($item['name'], /** @scrutinizer ignore-type */ $values, $defaultValues, $item['classname']);
Loading history...
197
198
                    // get content
199
                    $item['html'] = $rbt->parse();
200
                } elseif ($field['type'] === 'checkbox') {
201
                    // reset
202
                    $newValues = [];
203
204
                    // rebuild values
205
                    foreach ($values as $value) {
206
                        $newValues[] = ['label' => $value, 'value' => $value];
207
                    }
208
209
                    // create element
210
                    $chk = $this->form->addMultiCheckbox($item['name'], $newValues, $defaultValues, $item['classname']);
211
212
                    // get content
213
                    $item['html'] = $chk->parse();
214
                } elseif ($field['type'] === 'mailmotor') {
215
                    $chk = $this->form->addCheckbox($item['name'], false, $item['classname']);
216
217
                    // get content
218
                    $item['html'] = $chk->parse();
219
                } elseif ($field['type'] === 'textbox') {
220
                    // create element
221
                    $txt = $this->form->addText($item['name'], $defaultValues, 255, $item['classname']);
222
223
                    // add required attribute
224
                    if ($item['required']) {
225
                        $txt->setAttribute('required', null);
226
                    }
227
                    if (isset($item['validations']['email'])) {
228
                        $txt->setAttribute('type', 'email');
229
                    }
230
                    if (isset($item['validations']['number'])) {
231
                        $txt->setAttribute('type', 'number');
232
                    }
233
                    if ($item['placeholder']) {
234
                        $txt->setAttribute('placeholder', $item['placeholder']);
235
                    }
236
237
                    // add autocomplete attribute
238
                    if ($item['autocomplete']) {
239
                        $txt->setAttribute('autocomplete', $item['autocomplete']);
240
                    }
241
242
                    $this->setCustomHTML5ErrorMessages($item, $txt);
243
244
                    // get content
245
                    $item['html'] = $txt->parse();
246
                } elseif ($field['type'] === 'datetime') {
247
                    // create element
248
                    if ($field['settings']['input_type'] === 'date') {
249
                        // calculate default value
250
                        $amount = $field['settings']['value_amount'];
251
                        $type = $field['settings']['value_type'];
252
253
                        if ($type != '') {
254
                            switch ($type) {
255
                                case 'today':
256
                                    $defaultValues = date('Y-m-d'); // HTML5 input needs this format
257
                                    break;
258
                                case 'day':
259
                                case 'week':
260
                                case 'month':
261
                                case 'year':
262
                                    if ($amount != '') {
263
                                        $defaultValues = date('Y-m-d', strtotime('+' . $amount . ' ' . $type));
264
                                    }
265
                                    break;
266
                            }
267
                        }
268
269
                        // Convert the php date format to a jquery date format
270
                        $dateFormatShortJS = FrontendFormBuilderModel::convertPHPDateToJquery($this->get('fork.settings')->get('Core', 'date_format_short'));
271
272
                        $datetime = $this->form->addText($item['name'], $defaultValues, 255, 'inputDatefield ' . $item['classname'])->setAttributes(
273
                            [
274
                                'data-mask' => $dateFormatShortJS,
275
                                'data-firstday' => '1',
276
                                'type' => 'date',
277
                                'default-date' => (!empty($defaultValues) ? date($this->get('fork.settings')->get('Core', 'date_format_short'), strtotime($defaultValues)) : ''),
278
                            ]
279
                        );
280
                    } else {
281
                        $datetime = $this->form->addText($item['name'], $defaultValues, 255, $item['classname'])->setAttributes(['type' => 'time']);
282
                    }
283
284
                    // add required attribute
285
                    if ($item['required']) {
286
                        $datetime->setAttribute('required', null);
287
                    }
288
289
                    // add autocomplete attribute
290
                    if ($item['autocomplete']) {
291
                        $datetime->setAttribute('autocomplete', $item['autocomplete']);
292
                    }
293
294
                    $this->setCustomHTML5ErrorMessages($item, $datetime);
295
296
                    // get content
297
                    $item['html'] = $datetime->parse();
298
                } elseif ($field['type'] === 'textarea') {
299
                    // create element
300
                    $txt = $this->form->addTextarea($item['name'], $defaultValues, $item['classname']);
301
                    $txt->setAttribute('cols', 30);
302
303
                    // add required attribute
304
                    if ($item['required']) {
305
                        $txt->setAttribute('required', null);
306
                    }
307
                    if ($item['placeholder']) {
308
                        $txt->setAttribute('placeholder', $item['placeholder']);
309
                    }
310
311
                    $this->setCustomHTML5ErrorMessages($item, $txt);
312
313
                    // get content
314
                    $item['html'] = $txt->parse();
315
                } elseif ($field['type'] === 'heading') {
316
                    $item['html'] = '<h3>' . $values . '</h3>';
317
                } elseif ($field['type'] === 'paragraph') {
318
                    $item['html'] = $values;
319
                } elseif ($field['type'] === 'submit') {
320
                    $item['html'] = $values;
321
                } elseif ($field['type'] === 'recaptcha') {
322
                    $this->hasRecaptchaField = true;
323
                    continue;
324
                }
325
326
                // add to list
327
                $this->fieldsHTML[] = $item;
328
            }
329
        }
330
    }
331
332
    private function setCustomHTML5ErrorMessages(array $item, SpoonFormAttributes $formField): void
333
    {
334
        foreach ($item['validations'] as $validation) {
335
            $formField->setAttribute(
336
                'data-error-' . $validation['type'],
337
                $validation['error_message']
338
            );
339
        }
340
    }
341
342
    private function parse(): void
343
    {
344
        // form name
345
        $formName = 'form' . $this->item['id'];
346
        $this->template->assign('formName', $formName);
347
        $this->template->assign('formAction', $this->createAction() . '#' . $formName);
348
        $this->template->assign('successMessage', false);
349
350
        if ($this->hasRecaptchaField) {
351
            $this->header->addJS('https://www.google.com/recaptcha/api.js?hl=' . Locale::frontendLanguage());
352
            $this->template->assign('hasRecaptchaField', true);
353
            $this->template->assign(
354
                'googleRecaptchaVersion',
355
                FrontendModel::get('fork.settings')->get('Core', 'google_recaptcha_version', 'v2invisible')
356
            );
357
            $this->template->assign(
358
                'siteKey',
359
                FrontendModel::get('fork.settings')->get('Core', 'google_recaptcha_site_key')
360
            );
361
        }
362
363
        // got fields
364
        if (!empty($this->fieldsHTML)) {
365
            // value of the submit button
366
            $submitValue = '';
367
368
            // loop html fields
369
            foreach ($this->fieldsHTML as &$field) {
370
                if (in_array($field['type'], ['heading', 'paragraph', 'recaptcha'])) {
371
                    $field['plaintext'] = true;
372
                } elseif (in_array($field['type'], ['checkbox', 'radiobutton'])) {
373
                    // name (prefixed by type)
374
                    $name = ($field['type'] === 'checkbox') ?
375
                        'chk' . \SpoonFilter::toCamelCase($field['name']) :
376
                        'rbt' . \SpoonFilter::toCamelCase($field['name'])
377
                    ;
378
379
                    // rebuild so the html is stored in a general name (and not rbtName)
380
                    foreach ($field['html'] as &$item) {
381
                        $item['field'] = $item[$name];
382
                    }
383
384
                    // multiple items
385
                    $field['multiple'] = true;
386
                } elseif ($field['type'] === 'submit') {
387
                    $submitValue = $field['html'];
388
                } elseif ($field['type'] === 'mailmotor') {
389
                    $field['isMailmotor'] = true;
390
                } else {
391
                    $field['simple'] = true;
392
                }
393
394
                // errors (only for form elements)
395
                if (isset($field['simple']) || isset($field['multiple'])) {
396
                    $field['error'] = $this->form->getField(
397
                        $field['name']
398
                    )->getErrors();
399
                }
400
            }
401
402
            // assign
403
            $this->template->assign('submitValue', $submitValue);
404
            $this->template->assign('fields', $this->fieldsHTML);
405
406
            // parse form
407
            $this->form->parse($this->template);
408
            $this->template->assign('formToken', $this->form->getToken());
409
410
            // assign form error
411
            $this->template->assign('error', ($this->form->getErrors() != '' ? $this->form->getErrors() : false));
412
        }
413
    }
414
415
    private function parseSuccessMessage(): void
416
    {
417
        // form name
418
        $this->template->assign('formName', $this->formName);
419
        $this->template->assign('successMessage', $this->item['success_message']);
420
    }
421
422
    private function validateForm(): void
423
    {
424
        // submitted
425
        if ($this->form->isSubmitted()) {
426
            if ($this->hasRecaptchaField) {
427
                $request = $this->getRequest()->request;
428
                if (!$request->has('g-recaptcha-response')) {
429
                    $this->form->addError(FL::err('RecaptchaInvalid'));
430
                }
431
432
                $response = $request->get('g-recaptcha-response');
433
434
                $secret = FrontendModel::get('fork.settings')->get('Core', 'google_recaptcha_secret_key');
435
436
                if (!$secret) {
437
                    $this->form->addError(FL::err('RecaptchaInvalid'));
438
                }
439
440
                $recaptcha = new ReCaptcha($secret, new CurlPost());
441
442
                $response = $recaptcha->verify($response);
443
444
                if (!$response->isSuccess()) {
445
                    $this->form->addError(FL::err('RecaptchaInvalid'));
446
                }
447
            }
448
            // does the key exists?
449
            if (FrontendModel::getSession()->has('formbuilder_' . $this->item['id'])) {
450
                // calculate difference
451
                $diff = time() - (int) FrontendModel::getSession()->get('formbuilder_' . $this->item['id']);
452
453
                // calculate difference, it it isn't 10 seconds the we tell the user to slow down
454
                if ($diff < 10 && $diff != 0) {
455
                    $this->form->addError(FL::err('FormTimeout'));
456
                }
457
            }
458
459
            // validate fields
460
            foreach ($this->item['fields'] as $field) {
461
                // field name
462
                $fieldName = 'field' . $field['id'];
463
464
                // skip
465
                if (in_array($field['type'], ['submit', 'paragraph', 'heading', 'recaptcha'])) {
466
                    continue;
467
                }
468
469
                // loop other validations
470
                foreach ($field['validations'] as $rule => $settings) {
471
                    // already has an error so skip
472
                    if ($this->form->getField($fieldName)->getErrors() !== null) {
473
                        continue;
474
                    }
475
476
                    // required
477
                    if ($rule === 'required') {
478
                        $this->form->getField($fieldName)->isFilled($settings['error_message']);
479
                    } elseif ($rule === 'email') {
480
                        // only check this if the field is filled, if the field is required it will be validated before
481
                        if ($this->form->getField($fieldName)->isFilled()) {
482
                            $this->form->getField($fieldName)->isEmail(
483
                                $settings['error_message']
484
                            );
485
                        }
486
                    } elseif ($rule === 'number') {
487
                        // only check this if the field is filled, if the field is required it will be validated before
488
                        if ($this->form->getField($fieldName)->isFilled()) {
489
                            $this->form->getField($fieldName)->isNumeric(
490
                                $settings['error_message']
491
                            );
492
                        }
493
                    } elseif ($rule === 'time') {
494
                        $regexTime = '/^(([0-1][0-9]|2[0-3]|[0-9])|([0-1][0-9]|2[0-3]|[0-9])(:|h)[0-5]?[0-9]?)$/';
495
                        if (!\SpoonFilter::isValidAgainstRegexp($regexTime, $this->form->getField($fieldName)->getValue())) {
496
                            $this->form->getField($fieldName)->setError($settings['error_message']);
497
                        }
498
                    }
499
                }
500
            }
501
502
            // valid form
503
            if ($this->form->isCorrect()) {
504
                // item
505
                $data = [
506
                    'form_id' => $this->item['id'],
507
                    'session_id' => FrontendModel::getSession()->getId(),
508
                    'sent_on' => FrontendModel::getUTCDate(),
509
                    'data' => serialize(['server' => $_SERVER]),
510
                ];
511
512
                $dataId = null;
513
                // insert data
514
                if ($this->item['method'] !== 'email') {
515
                    $dataId = FrontendFormBuilderModel::insertData($data);
516
                }
517
518
                // init fields array
519
                $fields = [];
520
521
                // loop all fields
522
                foreach ($this->item['fields'] as $field) {
523
                    // skip
524
                    if (in_array($field['type'], ['submit', 'paragraph', 'heading', 'recaptcha', 'mailmotor'])) {
525
                        continue;
526
                    }
527
528
                    // field data
529
                    $fieldData = [];
530
                    $fieldData['data_id'] = $dataId;
531
                    $fieldData['label'] = $field['settings']['label'];
532
                    $fieldData['value'] = $this->form->getField('field' . $field['id'])->getValue();
533
534
                    if ($field['type'] === 'radiobutton') {
535
                        $values = [];
536
537
                        foreach ($field['settings']['values'] as $value) {
538
                            $values[$value['value']] = $value['label'];
539
                        }
540
541
                        $fieldData['value'] = array_key_exists($fieldData['value'], $values)
542
                            ? $values[$fieldData['value']] : null;
543
                    }
544
545
                    // clean up
546
                    if (is_array($fieldData['value']) && empty($fieldData['value'])) {
547
                        $fieldData['value'] = null;
548
                    }
549
550
                    // serialize
551
                    if ($fieldData['value'] !== null) {
552
                        $fieldData['value'] = serialize($fieldData['value']);
553
                    }
554
555
                    // save fields data
556
                    $fields[$field['id']] = $fieldData;
557
558
                    // insert
559
                    if ($this->item['method'] !== 'email') {
560
                        FrontendFormBuilderModel::insertDataField($fieldData);
561
                    }
562
                }
563
564
                $this->get('event_dispatcher')->dispatch(
565
                    FormBuilderEvents::FORM_SUBMITTED,
566
                    new FormBuilderSubmittedEvent($this->item, $fields, $dataId)
567
                );
568
569
                // store timestamp in session so we can block excessive usage
570
                FrontendModel::getSession()->set('formbuilder_' . $this->item['id'], time());
571
572
                // redirect
573
                $redirect = SITE_URL . $this->url->getQueryString();
574
                $redirect .= (stripos($redirect, '?') === false) ? '?' : '&';
575
                $redirect .= 'identifier=' . $this->item['identifier'];
576
                $redirect .= '#' . $this->formName;
577
578
                throw new RedirectException(
579
                    'Redirect',
580
                    new RedirectResponse($redirect)
581
                );
582
            } else {
583
                // not correct, show errors
584
                $this->template->assign('formBuilderError', FL::err('FormError'));
585
            }
586
        }
587
    }
588
}
589