Completed
Push — master ( 82acc4...deba42 )
by Will
13s
created

UserDefinedFormController::init()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 6
nop 0
1
<?php
2
3
namespace SilverStripe\UserForms\Control;
4
5
use PageController;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Assets\Upload;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Email\Email;
10
use SilverStripe\Control\HTTP;
11
use SilverStripe\Core\Manifest\Module;
12
use SilverStripe\Core\Manifest\ModuleLoader;
13
use SilverStripe\Forms\Form;
14
use SilverStripe\i18n\i18n;
15
use SilverStripe\ORM\ArrayList;
16
use SilverStripe\ORM\FieldType\DBField;
17
use SilverStripe\ORM\ValidationException;
18
use SilverStripe\Security\Security;
19
use SilverStripe\UserForms\Form\UserForm;
20
use SilverStripe\UserForms\Model\EditableFormField\EditableFileField;
21
use SilverStripe\UserForms\Model\Submission\SubmittedForm;
22
use SilverStripe\View\ArrayData;
23
use SilverStripe\View\Requirements;
24
use SilverStripe\View\SSViewer;
25
26
/**
27
 * Controller for the {@link UserDefinedForm} page type.
28
 *
29
 * @package userforms
30
 */
31
class UserDefinedFormController extends PageController
32
{
33
    private static $finished_anchor = '#uff';
0 ignored issues
show
Unused Code introduced by
The property $finished_anchor is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
34
35
    private static $allowed_actions = [
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
36
        'index',
37
        'ping',
38
        'Form',
39
        'finished'
40
    ];
41
42
    protected function init()
43
    {
44
        parent::init();
45
46
        $page = $this->data();
47
48
        $userforms = ModuleLoader::getModule('silverstripe/userforms');
49
        $admin = ModuleLoader::getModule('silverstripe/admin');
0 ignored issues
show
Unused Code introduced by
$admin is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
50
        // load the css
51
        if (!$page->config()->get('block_default_userforms_css')) {
52
            Requirements::css($userforms->getRelativeResourcePath('client/dist/styles/userforms.css'));
53
        }
54
55
        // load the jquery
56
        if (!$page->config()->get('block_default_userforms_js')) {
57
            Requirements::javascript('//code.jquery.com/jquery-1.7.2.min.js');
58
            Requirements::javascript(
59
                $userforms->getRelativeResourcePath('client/thirdparty/jquery-validate/jquery.validate.min.js')
60
            );
61
            Requirements::add_i18n_javascript($userforms->getRelativeResourcePath('client/lang'));
62
            Requirements::javascript($userforms->getRelativeResourcePath('client/dist/js/userforms.js'));
63
64
            $this->addUserFormsValidatei18n($userforms);
65
66
            // Bind a confirmation message when navigating away from a partially completed form.
67
            if ($page::config()->get('enable_are_you_sure')) {
68
                Requirements::javascript(
69
                    $userforms->getRelativeResourcePath('client/thirdparty/jquery.are-you-sure/jquery.are-you-sure.js')
70
                );
71
            }
72
        }
73
    }
74
75
    /**
76
     * Add the necessary jQuery validate i18n translation files, either by locale or by langauge,
77
     * e.g. 'en_NZ' or 'en'. This adds "methods_abc.min.js" as well as "messages_abc.min.js" from the
78
     * jQuery validate thirdparty library.
79
     *
80
     * @param Module $module
81
     */
82
    protected function addUserFormsValidatei18n(Module $module)
83
    {
84
        $candidates = [
85
            i18n::getData()->langFromLocale(i18n::config()->get('default_locale')),
86
            i18n::config()->get('default_locale'),
87
            i18n::getData()->langFromLocale(i18n::get_locale()),
88
            i18n::get_locale(),
89
        ];
90
91
        foreach ($candidates as $candidate) {
92
            foreach (['messages', 'methods'] as $candidateType) {
93
                $localisationCandidate = "client/thirdparty/jquery-validate/localization/{$candidateType}_{$candidate}.min.js";
94
                if ($module->hasResource($localisationCandidate)) {
95
                    Requirements::javascript($module->getRelativeResourcePath($localisationCandidate));
96
                }
97
            }
98
        }
99
    }
100
101
    /**
102
     * Using $UserDefinedForm in the Content area of the page shows
103
     * where the form should be rendered into. If it does not exist
104
     * then default back to $Form.
105
     *
106
     * @return array
107
     */
108
    public function index()
109
    {
110
        if ($this->Content && $form = $this->Form()) {
111
            $hasLocation = stristr($this->Content, '$UserDefinedForm');
112
            if ($hasLocation) {
113
                /** @see Requirements_Backend::escapeReplacement */
114
                $formEscapedForRegex = addcslashes($form->forTemplate(), '\\$');
115
                $content = preg_replace(
116
                    '/(<p[^>]*>)?\\$UserDefinedForm(<\\/p>)?/i',
117
                    $formEscapedForRegex,
118
                    $this->Content
119
                );
120
                return [
121
                    'Content' => DBField::create_field('HTMLText', $content),
122
                    'Form' => ''
123
                ];
124
            }
125
        }
126
127
        return [
128
            'Content' => DBField::create_field('HTMLText', $this->Content),
129
            'Form' => $this->Form()
130
        ];
131
    }
132
133
    /**
134
     * Keep the session alive for the user.
135
     *
136
     * @return int
137
     */
138
    public function ping()
139
    {
140
        return 1;
141
    }
142
143
    /**
144
     * Get the form for the page. Form can be modified by calling {@link updateForm()}
145
     * on a UserDefinedForm extension.
146
     *
147
     * @return Form
148
     */
149
    public function Form()
150
    {
151
        $form = UserForm::create($this, 'Form_' . $this->ID);
152
        /** @skipUpgrade */
153
        $form->setFormAction(Controller::join_links($this->Link(), 'Form'));
154
        $this->generateConditionalJavascript();
155
        return $form;
156
    }
157
158
    /**
159
     * Generate the javascript for the conditional field show / hiding logic.
160
     *
161
     * @return void
162
     */
163
    public function generateConditionalJavascript()
164
    {
165
        $default = '';
0 ignored issues
show
Unused Code introduced by
$default is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
166
        $rules = '';
167
168
        $watch = [];
169
170
        if ($this->Fields()) {
171
            /** @var EditableFormField $field */
172
            foreach ($this->Fields() as $field) {
173
                if ($result = $field->formatDisplayRules()) {
174
                    $watch[] = $result;
175
                }
176
            }
177
        }
178
        if ($watch) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $watch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
179
            $rules .= $this->buildWatchJS($watch);
180
        }
181
182
        // Only add customScript if $default or $rules is defined
183
        if ($rules) {
184
            Requirements::customScript(<<<JS
185
                (function($) {
186
                    $(document).ready(function() {
187
                        {$rules}
188
                    });
189
                })(jQuery);
190
JS
191
            , 'UserFormsConditional');
192
        }
193
    }
194
195
    /**
196
     * Process the form that is submitted through the site
197
     *
198
     * {@see UserForm::validate()} for validation step prior to processing
199
     *
200
     * @param array $data
201
     * @param Form $form
202
     *
203
     * @return \SilverStripe\Control\HTTPResponse
204
     */
205
    public function process($data, $form)
0 ignored issues
show
Coding Style introduced by
process uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
206
    {
207
        $submittedForm = SubmittedForm::create();
208
        $submittedForm->SubmittedByID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
209
        $submittedForm->ParentID = $this->ID;
210
211
        // if saving is not disabled save now to generate the ID
212
        if (!$this->DisableSaveSubmissions) {
213
            $submittedForm->write();
214
        }
215
216
        $attachments = array();
217
        $submittedFields = ArrayList::create();
218
219
        foreach ($this->Fields() as $field) {
220
            if (!$field->showInReports()) {
221
                continue;
222
            }
223
224
            $submittedField = $field->getSubmittedFormField();
225
            $submittedField->ParentID = $submittedForm->ID;
226
            $submittedField->Name = $field->Name;
227
            $submittedField->Title = $field->getField('Title');
228
229
            // save the value from the data
230
            if ($field->hasMethod('getValueFromData')) {
231
                $submittedField->Value = $field->getValueFromData($data);
232
            } else {
233
                if (isset($data[$field->Name])) {
234
                    $submittedField->Value = $data[$field->Name];
235
                }
236
            }
237
238
            if (!empty($data[$field->Name])) {
239
                if (in_array(EditableFileField::class, $field->getClassAncestry())) {
240
                    if (!empty($_FILES[$field->Name]['name'])) {
241
                        $foldername = $field->getFormField()->getFolderName();
242
243
                        // create the file from post data
244
                        $upload = Upload::create();
245
                        $file = File::create();
246
                        $file->ShowInSearch = 0;
247
                        try {
248
                            $upload->loadIntoFile($_FILES[$field->Name], $file, $foldername);
249
                        } catch (ValidationException $e) {
250
                            $validationResult = $e->getResult();
251
                            $form->addErrorMessage($field->Name, $validationResult->message(), 'bad');
0 ignored issues
show
Bug introduced by
The method message() does not exist on SilverStripe\ORM\ValidationResult. Did you maybe mean addFieldMessage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
252
                            Controller::curr()->redirectBack();
253
                            return;
254
                        }
255
256
                        // write file to form field
257
                        $submittedField->UploadedFileID = $file->ID;
258
259
                        // attach a file only if lower than 1MB
260
                        if ($file->getAbsoluteSize() < 1024 * 1024 * 1) {
261
                            $attachments[] = $file;
262
                        }
263
                    }
264
                }
265
            }
266
267
            $submittedField->extend('onPopulationFromField', $field);
268
269
            if (!$this->DisableSaveSubmissions) {
270
                $submittedField->write();
271
            }
272
273
            $submittedFields->push($submittedField);
274
        }
275
276
        $emailData = [
277
            'Sender' => Security::getCurrentUser(),
278
            'Fields' => $submittedFields
279
        ];
280
281
        $this->extend('updateEmailData', $emailData, $attachments);
282
283
        // email users on submit.
284
        if ($recipients = $this->FilteredEmailRecipients($data, $form)) {
285
            foreach ($recipients as $recipient) {
286
                $email = Email::create()
287
                    ->setHTMLTemplate('email/SubmittedFormEmail.ss')
288
                    ->setPlainTemplate('email/SubmittedFormEmail.ss');
289
290
                $mergeFields = $this->getMergeFieldsMap($emailData['Fields']);
291
292
                if ($attachments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attachments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
293
                    foreach ($attachments as $file) {
294
                        if (!$file->ID != 0) {
295
                            continue;
296
                        }
297
298
                        $email->attachFile(
299
                            $file->Filename,
300
                            $file->Filename,
301
                            HTTP::get_mime_type($file->Filename)
302
                        );
303
                    }
304
                }
305
306
                $parsedBody = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields);
307
308
                if (!$recipient->SendPlain && $recipient->emailTemplateExists()) {
309
                    $email->setHTMLTemplate($recipient->EmailTemplate);
310
                }
311
312
                $email->setData($recipient);
313
                foreach ($emailData as $key => $value) {
314
                    $email->addData($key, $value);
315
                }
316
317
                $email->setFrom($recipient->EmailFrom);
318
                $email->setBody($parsedBody);
319
                $email->setTo($recipient->EmailAddress);
320
                $email->setSubject($recipient->EmailSubject);
321
322
                if ($recipient->EmailReplyTo) {
323
                    $email->setReplyTo($recipient->EmailReplyTo);
324
                }
325
326
                // check to see if they are a dynamic reply to. eg based on a email field a user selected
327 View Code Duplication
                if ($recipient->SendEmailFromField()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name);
329
330
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
331
                        $email->setReplyTo($submittedFormField->Value);
332
                    }
333
                }
334
                // check to see if they are a dynamic reciever eg based on a dropdown field a user selected
335 View Code Duplication
                if ($recipient->SendEmailToField()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name);
337
338
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
339
                        $email->setTo($submittedFormField->Value);
340
                    }
341
                }
342
343
                // check to see if there is a dynamic subject
344 View Code Duplication
                if ($recipient->SendEmailSubjectField()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
345
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name);
346
347
                    if ($submittedFormField && trim($submittedFormField->Value)) {
348
                        $email->setSubject($submittedFormField->Value);
349
                    }
350
                }
351
352
                $this->extend('updateEmail', $email, $recipient, $emailData);
353
354
                if ($recipient->SendPlain) {
355
                    $body = strip_tags($recipient->getEmailBodyContent()) . "\n";
356
                    if (isset($emailData['Fields']) && !$recipient->HideFormData) {
357
                        foreach ($emailData['Fields'] as $Field) {
358
                            $body .= $Field->Title . ': ' . $Field->Value . " \n";
359
                        }
360
                    }
361
362
                    $email->setBody($body);
363
                    $email->sendPlain();
364
                } else {
365
                    $email->send();
366
                }
367
            }
368
        }
369
370
        $submittedForm->extend('updateAfterProcess');
371
372
        $session = $this->getRequest()->getSession();
373
        $session->clear("FormInfo.{$form->FormName()}.errors");
374
        $session->clear("FormInfo.{$form->FormName()}.data");
375
376
        $referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : "";
377
378
        // set a session variable from the security ID to stop people accessing
379
        // the finished method directly.
380
        if (!$this->DisableAuthenicatedFinishAction) {
381
            if (isset($data['SecurityID'])) {
382
                $session->set('FormProcessed', $data['SecurityID']);
383
            } else {
384
                // if the form has had tokens disabled we still need to set FormProcessed
385
                // to allow us to get through the finshed method
386
                if (!$this->Form()->getSecurityToken()->isEnabled()) {
387
                    $randNum = rand(1, 1000);
388
                    $randHash = md5($randNum);
389
                    $session->set('FormProcessed', $randHash);
390
                    $session->set('FormProcessedNum', $randNum);
391
                }
392
            }
393
        }
394
395
        if (!$this->DisableSaveSubmissions) {
396
            $session->set('userformssubmission'. $this->ID, $submittedForm->ID);
397
        }
398
399
        return $this->redirect($this->Link('finished') . $referrer . $this->config()->get('finished_anchor'));
400
    }
401
402
    /**
403
     * Allows the use of field values in email body.
404
     *
405
     * @param ArrayList $fields
406
     * @return ArrayData
407
     */
408
    protected function getMergeFieldsMap($fields = [])
409
    {
410
        $data = ArrayData::create([]);
411
412
        foreach ($fields as $field) {
413
            $data->setField($field->Name, DBField::create_field('Text', $field->Value));
414
        }
415
416
        return $data;
417
    }
418
419
    /**
420
     * This action handles rendering the "finished" message, which is
421
     * customizable by editing the ReceivedFormSubmission template.
422
     *
423
     * @return ViewableData
424
     */
425
    public function finished()
0 ignored issues
show
Coding Style introduced by
finished uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
426
    {
427
        $submission = $this->getRequest()->getSession()->get('userformssubmission'. $this->ID);
428
429
        if ($submission) {
430
            $submission = SubmittedForm::get()->byId($submission);
431
        }
432
433
        $referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null;
434
435
        if (!$this->DisableAuthenicatedFinishAction) {
436
            $formProcessed = $this->getRequest()->getSession()->get('FormProcessed');
437
438
            if (!isset($formProcessed)) {
439
                return $this->redirect($this->Link() . $referrer);
440
            } else {
441
                $securityID = $this->getRequest()->getSession()->get('SecurityID');
442
                // make sure the session matches the SecurityID and is not left over from another form
443
                if ($formProcessed != $securityID) {
444
                    // they may have disabled tokens on the form
445
                    $securityID = md5($this->getRequest()->getSession()->get('FormProcessedNum'));
446
                    if ($formProcessed != $securityID) {
447
                        return $this->redirect($this->Link() . $referrer);
448
                    }
449
                }
450
            }
451
452
            $this->getRequest()->getSession()->clear('FormProcessed');
453
        }
454
455
        $data = [
456
            'Submission' => $submission,
457
            'Link' => $referrer
458
        ];
459
460
        $this->extend('updateReceivedFormSubmissionData', $data);
461
462
        return $this->customise([
463
            'Content' => $this->customise($data)->renderWith(__CLASS__ . '_ReceivedFormSubmission'),
464
            'Form' => '',
465
        ]);
466
    }
467
468
    /**
469
     * Outputs the required JS from the $watch input
470
     *
471
     * @param array $watch
472
     *
473
     * @return string
474
     */
475
    protected function buildWatchJS($watch)
476
    {
477
        $result = '';
478
        foreach ($watch as $key => $rule) {
479
            $events = implode(' ', $rule['events']);
480
            $selectors = implode(', ', $rule['selectors']);
481
            $conjunction = $rule['conjunction'];
482
            $operations = implode(" {$conjunction} ", $rule['operations']);
483
            $target = $rule['targetFieldID'];
484
485
            $result .= <<<EOS
486
\n
487
    $('.userform').on('{$events}',
488
    "{$selectors}",
489
    function (){
490
        if ({$operations}) {
491
            $('{$target}').{$rule['view']};
492
        } else {
493
            $('{$target}').{$rule['opposite']};
494
        }
495
    });
496
    $("{$target}").find('.hide').removeClass('hide');
497
EOS;
498
        }
499
500
        return $result;
501
    }
502
}
503