UserDefinedFormController   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 529
Duplicated Lines 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 257
c 9
b 0
f 0
dl 0
loc 529
rs 2
wmc 81

10 Methods

Rating   Name   Duplication   Size   Complexity  
A ping() 0 3 1
A index() 0 23 5
A init() 0 27 4
A buildWatchJS() 0 29 2
F process() 0 242 48
B finished() 0 40 7
A Form() 0 7 1
B generateConditionalJavascript() 0 33 7
A addUserFormsValidatei18n() 0 18 4
A getMergeFieldsMap() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like UserDefinedFormController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UserDefinedFormController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\UserForms\Control;
4
5
use PageController;
0 ignored issues
show
Bug introduced by
The type PageController was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Psr\Log\LoggerInterface;
7
use SilverStripe\AssetAdmin\Controller\AssetAdmin;
0 ignored issues
show
Bug introduced by
The type SilverStripe\AssetAdmin\Controller\AssetAdmin was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use SilverStripe\Assets\File;
9
use SilverStripe\Assets\Upload;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Email\Email;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\HTTPResponse;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Core\Manifest\ModuleLoader;
16
use SilverStripe\Forms\Form;
17
use SilverStripe\i18n\i18n;
18
use SilverStripe\ORM\ArrayList;
19
use SilverStripe\ORM\FieldType\DBField;
20
use SilverStripe\ORM\ValidationException;
21
use SilverStripe\ORM\ValidationResult;
22
use SilverStripe\Security\Security;
23
use SilverStripe\UserForms\Extension\UserFormFileExtension;
24
use SilverStripe\UserForms\Form\UserForm;
25
use SilverStripe\UserForms\Model\EditableFormField;
26
use SilverStripe\UserForms\Model\EditableFormField\EditableFileField;
27
use SilverStripe\UserForms\Model\Submission\SubmittedForm;
28
use SilverStripe\UserForms\Model\Submission\SubmittedFileField;
29
use SilverStripe\UserForms\Model\UserDefinedForm;
30
use SilverStripe\View\ArrayData;
31
use SilverStripe\View\Requirements;
32
use SilverStripe\View\SSViewer;
33
use SilverStripe\View\ViewableData;
34
use Swift_RfcComplianceException;
35
36
/**
37
 * Controller for the {@link UserDefinedForm} page type.
38
 *
39
 * @package userforms
40
 */
41
class UserDefinedFormController extends PageController
42
{
43
    private static $finished_anchor = '#uff';
0 ignored issues
show
introduced by
The private property $finished_anchor is not used, and could be removed.
Loading history...
44
45
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
46
        'index',
47
        'ping',
48
        'Form',
49
        'finished',
50
    ];
51
52
    /** @var string The name of the folder where form submissions will be placed by default */
53
    private static $form_submissions_folder = 'Form-submissions';
0 ignored issues
show
introduced by
The private property $form_submissions_folder is not used, and could be removed.
Loading history...
54
55
    protected function init()
56
    {
57
        parent::init();
58
59
        $page = $this->data();
60
61
        // load the css
62
        if (!$page->config()->get('block_default_userforms_css')) {
63
            Requirements::css('silverstripe/userforms:client/dist/styles/userforms.css');
64
        }
65
66
        // load the jquery
67
        if (!$page->config()->get('block_default_userforms_js')) {
68
            Requirements::javascript('//code.jquery.com/jquery-3.4.1.min.js');
69
            Requirements::javascript(
70
                'silverstripe/userforms:client/thirdparty/jquery-validate/jquery.validate.min.js'
71
            );
72
            Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js');
73
            Requirements::add_i18n_javascript('silverstripe/userforms:client/lang');
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Requir...::add_i18n_javascript() has been deprecated. ( Ignorable by Annotation )

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

73
            /** @scrutinizer ignore-deprecated */ Requirements::add_i18n_javascript('silverstripe/userforms:client/lang');
Loading history...
74
            Requirements::javascript('silverstripe/userforms:client/dist/js/userforms.js');
75
76
            $this->addUserFormsValidatei18n();
77
78
            // Bind a confirmation message when navigating away from a partially completed form.
79
            if ($page::config()->get('enable_are_you_sure')) {
80
                Requirements::javascript(
81
                    'silverstripe/userforms:client/thirdparty/jquery.are-you-sure/jquery.are-you-sure.js'
82
                );
83
            }
84
        }
85
    }
86
87
    /**
88
     * Add the necessary jQuery validate i18n translation files, either by locale or by langauge,
89
     * e.g. 'en_NZ' or 'en'. This adds "methods_abc.min.js" as well as "messages_abc.min.js" from the
90
     * jQuery validate thirdparty library.
91
     */
92
    protected function addUserFormsValidatei18n()
93
    {
94
        $module = ModuleLoader::getModule('silverstripe/userforms');
95
96
        $candidates = [
97
            i18n::getData()->langFromLocale(i18n::config()->get('default_locale')),
98
            i18n::config()->get('default_locale'),
99
            i18n::getData()->langFromLocale(i18n::get_locale()),
100
            i18n::get_locale(),
101
        ];
102
103
        foreach ($candidates as $candidate) {
104
            foreach (['messages', 'methods'] as $candidateType) {
105
                $localisationCandidate = "client/thirdparty/jquery-validate/localization/{$candidateType}_{$candidate}.min.js";
106
107
                $resource = $module->getResource($localisationCandidate);
108
                if ($resource->exists()) {
109
                    Requirements::javascript($resource->getRelativePath());
110
                }
111
            }
112
        }
113
    }
114
115
    /**
116
     * Using $UserDefinedForm in the Content area of the page shows
117
     * where the form should be rendered into. If it does not exist
118
     * then default back to $Form.
119
     *
120
     * @return array
121
     */
122
    public function index(HTTPRequest $request = null)
123
    {
124
        $form = $this->Form();
125
        if ($this->Content && $form && !$this->config()->disable_form_content_shortcode) {
126
            $hasLocation = stristr($this->Content, '$UserDefinedForm');
127
            if ($hasLocation) {
128
                /** @see Requirements_Backend::escapeReplacement */
129
                $formEscapedForRegex = addcslashes($form->forTemplate(), '\\$');
130
                $content = preg_replace(
131
                    '/(<p[^>]*>)?\\$UserDefinedForm(<\\/p>)?/i',
132
                    $formEscapedForRegex,
133
                    $this->Content
134
                );
135
                return [
136
                    'Content' => DBField::create_field('HTMLText', $content),
137
                    'Form' => ''
138
                ];
139
            }
140
        }
141
142
        return [
143
            'Content' => DBField::create_field('HTMLText', $this->Content),
144
            'Form' => $this->Form()
145
        ];
146
    }
147
148
    /**
149
     * Keep the session alive for the user.
150
     *
151
     * @return int
152
     */
153
    public function ping()
154
    {
155
        return 1;
156
    }
157
158
    /**
159
     * Get the form for the page. Form can be modified by calling {@link updateForm()}
160
     * on a UserDefinedForm extension.
161
     *
162
     * @return Form
163
     */
164
    public function Form()
165
    {
166
        $form = UserForm::create($this, 'Form_' . $this->ID);
167
        /** @skipUpgrade */
168
        $form->setFormAction(Controller::join_links($this->Link(), 'Form'));
169
        $this->generateConditionalJavascript();
170
        return $form;
171
    }
172
173
    /**
174
     * Generate the javascript for the conditional field show / hiding logic.
175
     *
176
     * @return void
177
     */
178
    public function generateConditionalJavascript()
179
    {
180
        $rules = '';
181
        $form = $this->data();
182
        if (!$form) {
183
            return;
184
        }
185
        $formFields = $form->Fields();
186
187
        $watch = [];
188
189
        if ($formFields) {
190
            /** @var EditableFormField $field */
191
            foreach ($formFields as $field) {
192
                if ($result = $field->formatDisplayRules()) {
193
                    $watch[] = $result;
194
                }
195
            }
196
        }
197
        if ($watch) {
198
            $rules .= $this->buildWatchJS($watch);
199
        }
200
201
        // Only add customScript if $default or $rules is defined
202
        if ($rules) {
203
            Requirements::customScript(<<<JS
204
                (function($) {
205
                    $(document).ready(function() {
206
                        {$rules}
207
                    });
208
                })(jQuery);
209
JS
210
                , 'UserFormsConditional-' . $form->ID);
211
        }
212
    }
213
214
    /**
215
     * Process the form that is submitted through the site
216
     *
217
     * {@see UserForm::validate()} for validation step prior to processing
218
     *
219
     * @param array $data
220
     * @param Form $form
221
     *
222
     * @return HTTPResponse
223
     */
224
    public function process($data, $form)
225
    {
226
        $submittedForm = SubmittedForm::create();
227
        $submittedForm->SubmittedByID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
228
        $submittedForm->ParentClass = get_class($this->data());
229
        $submittedForm->ParentID = $this->ID;
230
231
        // if saving is not disabled save now to generate the ID
232
        if (!$this->DisableSaveSubmissions) {
233
            $submittedForm->write();
234
        }
235
236
        $attachments = [];
237
        $submittedFields = ArrayList::create();
238
239
        foreach ($this->data()->Fields() as $field) {
240
            if (!$field->showInReports()) {
241
                continue;
242
            }
243
244
            $submittedField = $field->getSubmittedFormField();
245
            $submittedField->ParentID = $submittedForm->ID;
246
            $submittedField->Name = $field->Name;
247
            $submittedField->Title = $field->getField('Title');
248
249
            // save the value from the data
250
            if ($field->hasMethod('getValueFromData')) {
251
                $submittedField->Value = $field->getValueFromData($data);
252
            } else {
253
                if (isset($data[$field->Name])) {
254
                    $submittedField->Value = $data[$field->Name];
255
                }
256
            }
257
258
            if (!empty($data[$field->Name])) {
259
                if (in_array(EditableFileField::class, $field->getClassAncestry())) {
260
                    if (!empty($_FILES[$field->Name]['name'])) {
261
                        $foldername = $field->getFormField()->getFolderName();
262
263
                        // create the file from post data
264
                        $upload = Upload::create();
265
                        try {
266
                            $upload->loadIntoFile($_FILES[$field->Name], null, $foldername);
267
                        } catch (ValidationException $e) {
268
                            $validationResult = $e->getResult();
269
                            foreach ($validationResult->getMessages() as $message) {
270
                                $form->sessionMessage($message['message'], ValidationResult::TYPE_ERROR);
271
                            }
272
                            Controller::curr()->redirectBack();
273
                            return;
274
                        }
275
                        /** @var AssetContainer|File $file */
276
                        $file = $upload->getFile();
277
                        $file->ShowInSearch = 0;
278
                        $file->UserFormUpload = UserFormFileExtension::USER_FORM_UPLOAD_TRUE;
279
                        $file->write();
280
281
                        // generate image thumbnail to show in asset-admin
282
                        // you can run userforms without asset-admin, so need to ensure asset-admin is installed
283
                        if (class_exists(AssetAdmin::class)) {
284
                            AssetAdmin::singleton()->generateThumbnails($file);
285
                        }
286
287
                        // write file to form field
288
                        $submittedField->UploadedFileID = $file->ID;
289
290
                        // attach a file only if lower than 1MB
291
                        if ($file->getAbsoluteSize() < 1024 * 1024 * 1) {
292
                            $attachments[] = $file;
293
                        }
294
                    }
295
                }
296
            }
297
298
            $submittedField->extend('onPopulationFromField', $field);
299
300
            if (!$this->DisableSaveSubmissions) {
301
                $submittedField->write();
302
            }
303
304
            $submittedFields->push($submittedField);
305
        }
306
307
        $emailData = [
308
            'Sender' => Security::getCurrentUser(),
309
            'HideFormData' => false,
310
            'Fields' => $submittedFields,
311
            'Body' => '',
312
        ];
313
314
        $this->extend('updateEmailData', $emailData, $attachments);
315
316
        // email users on submit.
317
        if ($recipients = $this->FilteredEmailRecipients($data, $form)) {
318
            foreach ($recipients as $recipient) {
319
                $email = Email::create()
320
                    ->setHTMLTemplate('email/SubmittedFormEmail')
321
                    ->setPlainTemplate('email/SubmittedFormEmailPlain');
322
323
                // Merge fields are used for CMS authors to reference specific form fields in email content
324
                $mergeFields = $this->getMergeFieldsMap($emailData['Fields']);
325
326
                if ($attachments) {
327
                    foreach ($attachments as $file) {
328
                        /** @var File $file */
329
                        if ((int) $file->ID === 0) {
330
                            continue;
331
                        }
332
333
                        $email->addAttachmentFromData(
334
                            $file->getString(),
335
                            $file->getFilename(),
336
                            $file->getMimeType()
337
                        );
338
                    }
339
                }
340
341
                if (!$recipient->SendPlain && $recipient->emailTemplateExists()) {
342
                    $email->setHTMLTemplate($recipient->EmailTemplate);
343
                }
344
345
                // Add specific template data for the current recipient
346
                $emailData['HideFormData'] =  (bool) $recipient->HideFormData;
347
                // Include any parsed merge field references from the CMS editor - this is already escaped
348
                $emailData['Body'] = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields);
349
350
                // Push the template data to the Email's data
351
                foreach ($emailData as $key => $value) {
352
                    $email->addData($key, $value);
353
                }
354
355
                // check to see if they are a dynamic reply to. eg based on a email field a user selected
356
                $emailFrom = $recipient->SendEmailFromField();
357
                if ($emailFrom && $emailFrom->exists()) {
358
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name);
359
360
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
361
                        $email->setReplyTo(explode(',', $submittedFormField->Value));
362
                    }
363
                } elseif ($recipient->EmailReplyTo) {
364
                    $email->setReplyTo(explode(',', $recipient->EmailReplyTo));
365
                }
366
367
                // check for a specified from; otherwise fall back to server defaults
368
                if ($recipient->EmailFrom) {
369
                    $email->setFrom(explode(',', $recipient->EmailFrom));
370
                }
371
372
                // check to see if they are a dynamic reciever eg based on a dropdown field a user selected
373
                $emailTo = $recipient->SendEmailToField();
374
375
                try {
376
                    if ($emailTo && $emailTo->exists()) {
377
                        $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name);
378
379
                        if ($submittedFormField && is_string($submittedFormField->Value)) {
380
                            $email->setTo(explode(',', $submittedFormField->Value));
381
                        } else {
382
                            $email->setTo(explode(',', $recipient->EmailAddress));
383
                        }
384
                    } else {
385
                        $email->setTo(explode(',', $recipient->EmailAddress));
386
                    }
387
                } catch (Swift_RfcComplianceException $e) {
388
                    // The sending address is empty and/or invalid. Log and skip sending.
389
                    $error = sprintf(
390
                        'Failed to set sender for userform submission %s: %s',
391
                        $submittedForm->ID,
392
                        $e->getMessage()
393
                    );
394
395
                    Injector::inst()->get(LoggerInterface::class)->notice($error);
396
397
                    continue;
398
                }
399
400
                // check to see if there is a dynamic subject
401
                $emailSubject = $recipient->SendEmailSubjectField();
402
                if ($emailSubject && $emailSubject->exists()) {
403
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name);
404
405
                    if ($submittedFormField && trim($submittedFormField->Value)) {
406
                        $email->setSubject($submittedFormField->Value);
407
                    } else {
408
                        $email->setSubject(SSViewer::execute_string($recipient->EmailSubject, $mergeFields));
409
                    }
410
                } else {
411
                    $email->setSubject(SSViewer::execute_string($recipient->EmailSubject, $mergeFields));
412
                }
413
414
                $this->extend('updateEmail', $email, $recipient, $emailData);
415
416
                if ((bool)$recipient->SendPlain) {
417
                    $body = strip_tags($recipient->getEmailBodyContent()) . "\n";
418
                    if (isset($emailData['Fields']) && !$emailData['HideFormData']) {
419
                        foreach ($emailData['Fields'] as $field) {
420
                            if ($field instanceof SubmittedFileField) {
421
                                $body .= $field->Title . ': ' . $field->ExportValue ." \n";
0 ignored issues
show
Bug Best Practice introduced by
The property ExportValue does not exist on SilverStripe\UserForms\M...sion\SubmittedFileField. Since you implemented __get, consider adding a @property annotation.
Loading history...
422
                            } else {
423
                                $body .= $field->Title . ': ' . $field->Value . " \n";
424
                            }
425
                        }
426
                    }
427
428
                    $email->setBody($body);
429
                    $email->sendPlain();
430
                } else {
431
                    $email->send();
432
                }
433
            }
434
        }
435
436
        $submittedForm->extend('updateAfterProcess');
437
438
        $session = $this->getRequest()->getSession();
439
        $session->clear("FormInfo.{$form->FormName()}.errors");
440
        $session->clear("FormInfo.{$form->FormName()}.data");
441
442
        $referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : "";
443
444
        // set a session variable from the security ID to stop people accessing
445
        // the finished method directly.
446
        if (!$this->DisableAuthenicatedFinishAction) {
447
            if (isset($data['SecurityID'])) {
448
                $session->set('FormProcessed', $data['SecurityID']);
449
            } else {
450
                // if the form has had tokens disabled we still need to set FormProcessed
451
                // to allow us to get through the finshed method
452
                if (!$this->Form()->getSecurityToken()->isEnabled()) {
453
                    $randNum = rand(1, 1000);
454
                    $randHash = md5($randNum);
455
                    $session->set('FormProcessed', $randHash);
456
                    $session->set('FormProcessedNum', $randNum);
457
                }
458
            }
459
        }
460
461
        if (!$this->DisableSaveSubmissions) {
462
            $session->set('userformssubmission'. $this->ID, $submittedForm->ID);
463
        }
464
465
        return $this->redirect($this->Link('finished') . $referrer . $this->config()->get('finished_anchor'));
466
    }
467
468
    /**
469
     * Allows the use of field values in email body.
470
     *
471
     * @param ArrayList $fields
472
     * @return ArrayData
473
     */
474
    protected function getMergeFieldsMap($fields = [])
475
    {
476
        $data = ArrayData::create([]);
477
478
        foreach ($fields as $field) {
479
            $data->setField($field->Name, DBField::create_field('Text', $field->Value));
480
        }
481
482
        return $data;
483
    }
484
485
    /**
486
     * This action handles rendering the "finished" message, which is
487
     * customizable by editing the ReceivedFormSubmission template.
488
     *
489
     * @return ViewableData
490
     */
491
    public function finished()
492
    {
493
        $submission = $this->getRequest()->getSession()->get('userformssubmission'. $this->ID);
494
495
        if ($submission) {
496
            $submission = SubmittedForm::get()->byId($submission);
497
        }
498
499
        $referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null;
500
501
        if (!$this->DisableAuthenicatedFinishAction) {
502
            $formProcessed = $this->getRequest()->getSession()->get('FormProcessed');
503
504
            if (!isset($formProcessed)) {
505
                return $this->redirect($this->Link() . $referrer);
506
            } else {
507
                $securityID = $this->getRequest()->getSession()->get('SecurityID');
508
                // make sure the session matches the SecurityID and is not left over from another form
509
                if ($formProcessed != $securityID) {
510
                    // they may have disabled tokens on the form
511
                    $securityID = md5($this->getRequest()->getSession()->get('FormProcessedNum'));
512
                    if ($formProcessed != $securityID) {
513
                        return $this->redirect($this->Link() . $referrer);
514
                    }
515
                }
516
            }
517
518
            $this->getRequest()->getSession()->clear('FormProcessed');
519
        }
520
521
        $data = [
522
            'Submission' => $submission,
523
            'Link' => $referrer
524
        ];
525
526
        $this->extend('updateReceivedFormSubmissionData', $data);
527
528
        return $this->customise([
529
            'Content' => $this->customise($data)->renderWith(__CLASS__ . '_ReceivedFormSubmission'),
530
            'Form' => '',
531
        ]);
532
    }
533
534
    /**
535
     * Outputs the required JS from the $watch input
536
     *
537
     * @param array $watch
538
     *
539
     * @return string
540
     */
541
    protected function buildWatchJS($watch)
542
    {
543
        $result = '';
544
        foreach ($watch as $key => $rule) {
545
            $events = implode(' ', $rule['events']);
546
            $selectors = implode(', ', $rule['selectors']);
547
            $conjunction = $rule['conjunction'];
548
            $operations = implode(" {$conjunction} ", $rule['operations']);
549
            $target = $rule['targetFieldID'];
550
            $holder = $rule['holder'];
551
552
            $result .= <<<EOS
553
\n
554
    $('.userform').on('{$events}',
555
    "{$selectors}",
556
    function (){
557
        if ({$operations}) {
558
            $('{$target}').{$rule['view']};
559
            {$holder}.{$rule['view']}.trigger('{$rule['holder_event']}');
560
        } else {
561
            $('{$target}').{$rule['opposite']};
562
            {$holder}.{$rule['opposite']}.trigger('{$rule['holder_event_opposite']}');
563
        }
564
    });
565
    $("{$target}").find('.hide').removeClass('hide');
566
EOS;
567
        }
568
569
        return $result;
570
    }
571
}
572