Completed
Push — master ( 016a0b...25fc98 )
by Franco
12s
created

UserDefinedFormController::index()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 14
nc 3
nop 0
1
<?php
2
3
namespace SilverStripe\UserForms\Model;
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
                $content = preg_replace(
114
                    '/(<p[^>]*>)?\\$UserDefinedForm(<\\/p>)?/i',
115
                    $form->forTemplate(),
116
                    $this->Content
117
                );
118
                return [
119
                    'Content' => DBField::create_field('HTMLText', $content),
120
                    'Form' => ''
121
                ];
122
            }
123
        }
124
125
        return [
126
            'Content' => DBField::create_field('HTMLText', $this->Content),
127
            'Form' => $this->Form()
128
        ];
129
    }
130
131
    /**
132
     * Keep the session alive for the user.
133
     *
134
     * @return int
135
     */
136
    public function ping()
137
    {
138
        return 1;
139
    }
140
141
    /**
142
     * Get the form for the page. Form can be modified by calling {@link updateForm()}
143
     * on a UserDefinedForm extension.
144
     *
145
     * @return Form
146
     */
147
    public function Form()
148
    {
149
        $form = UserForm::create($this, 'Form_' . $this->ID);
150
        /** @skipUpgrade */
151
        $form->setFormAction(Controller::join_links($this->Link(), 'Form'));
152
        $this->generateConditionalJavascript();
153
        return $form;
154
    }
155
156
    /**
157
     * Generate the javascript for the conditional field show / hiding logic.
158
     *
159
     * @return void
160
     */
161
    public function generateConditionalJavascript()
162
    {
163
        $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...
164
        $rules = '';
165
166
        $watch = [];
167
168
        if ($this->Fields()) {
169
            /** @var EditableFormField $field */
170
            foreach ($this->Fields() as $field) {
171
                if ($result = $field->formatDisplayRules()) {
172
                    $watch[] = $result;
173
                }
174
            }
175
        }
176
        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...
177
            $rules .= $this->buildWatchJS($watch);
178
        }
179
180
        // Only add customScript if $default or $rules is defined
181
        if ($rules) {
182
            Requirements::customScript(<<<JS
183
                (function($) {
184
                    $(document).ready(function() {
185
                        {$rules}
186
                    });
187
                })(jQuery);
188
JS
189
            , 'UserFormsConditional');
190
        }
191
    }
192
193
    /**
194
     * Process the form that is submitted through the site
195
     *
196
     * {@see UserForm::validate()} for validation step prior to processing
197
     *
198
     * @param array $data
199
     * @param Form $form
200
     *
201
     * @return \SilverStripe\Control\HTTPResponse
202
     */
203
    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...
204
    {
205
        $submittedForm = SubmittedForm::create();
206
        $submittedForm->SubmittedByID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
207
        $submittedForm->ParentID = $this->ID;
208
209
        // if saving is not disabled save now to generate the ID
210
        if (!$this->DisableSaveSubmissions) {
211
            $submittedForm->write();
212
        }
213
214
        $attachments = array();
215
        $submittedFields = ArrayList::create();
216
217
        foreach ($this->Fields() as $field) {
218
            if (!$field->showInReports()) {
219
                continue;
220
            }
221
222
            $submittedField = $field->getSubmittedFormField();
223
            $submittedField->ParentID = $submittedForm->ID;
224
            $submittedField->Name = $field->Name;
225
            $submittedField->Title = $field->getField('Title');
226
227
            // save the value from the data
228
            if ($field->hasMethod('getValueFromData')) {
229
                $submittedField->Value = $field->getValueFromData($data);
230
            } else {
231
                if (isset($data[$field->Name])) {
232
                    $submittedField->Value = $data[$field->Name];
233
                }
234
            }
235
236
            if (!empty($data[$field->Name])) {
237
                if (in_array(EditableFileField::class, $field->getClassAncestry())) {
238
                    if (!empty($_FILES[$field->Name]['name'])) {
239
                        $foldername = $field->getFormField()->getFolderName();
240
241
                        // create the file from post data
242
                        $upload = Upload::create();
243
                        $file = File::create();
244
                        $file->ShowInSearch = 0;
245
                        try {
246
                            $upload->loadIntoFile($_FILES[$field->Name], $file, $foldername);
247
                        } catch (ValidationException $e) {
248
                            $validationResult = $e->getResult();
249
                            $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...
250
                            Controller::curr()->redirectBack();
251
                            return;
252
                        }
253
254
                        // write file to form field
255
                        $submittedField->UploadedFileID = $file->ID;
256
257
                        // attach a file only if lower than 1MB
258
                        if ($file->getAbsoluteSize() < 1024 * 1024 * 1) {
259
                            $attachments[] = $file;
260
                        }
261
                    }
262
                }
263
            }
264
265
            $submittedField->extend('onPopulationFromField', $field);
266
267
            if (!$this->DisableSaveSubmissions) {
268
                $submittedField->write();
269
            }
270
271
            $submittedFields->push($submittedField);
272
        }
273
274
        $emailData = [
275
            'Sender' => Security::getCurrentUser(),
276
            'Fields' => $submittedFields
277
        ];
278
279
        $this->extend('updateEmailData', $emailData, $attachments);
280
281
        // email users on submit.
282
        if ($recipients = $this->FilteredEmailRecipients($data, $form)) {
283
            foreach ($recipients as $recipient) {
284
                $email = Email::create()
285
                    ->setHTMLTemplate('email/SubmittedFormEmail.ss')
286
                    ->setPlainTemplate('email/SubmittedFormEmail.ss');
287
288
                $mergeFields = $this->getMergeFieldsMap($emailData['Fields']);
289
290
                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...
291
                    foreach ($attachments as $file) {
292
                        if (!$file->ID != 0) {
293
                            continue;
294
                        }
295
296
                        $email->attachFile(
297
                            $file->Filename,
298
                            $file->Filename,
299
                            HTTP::get_mime_type($file->Filename)
300
                        );
301
                    }
302
                }
303
304
                $parsedBody = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields);
305
306
                if (!$recipient->SendPlain && $recipient->emailTemplateExists()) {
307
                    $email->setHTMLTemplate($recipient->EmailTemplate);
308
                }
309
310
                $email->setData($recipient);
311
                foreach ($emailData as $key => $value) {
312
                    $email->addData($key, $value);
313
                }
314
315
                $email->setFrom($recipient->EmailFrom);
316
                $email->setBody($parsedBody);
317
                $email->setTo($recipient->EmailAddress);
318
                $email->setSubject($recipient->EmailSubject);
319
320
                if ($recipient->EmailReplyTo) {
321
                    $email->setReplyTo($recipient->EmailReplyTo);
322
                }
323
324
                // check to see if they are a dynamic reply to. eg based on a email field a user selected
325 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...
326
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name);
327
328
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
329
                        $email->setReplyTo($submittedFormField->Value);
330
                    }
331
                }
332
                // check to see if they are a dynamic reciever eg based on a dropdown field a user selected
333 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...
334
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name);
335
336
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
337
                        $email->setTo($submittedFormField->Value);
338
                    }
339
                }
340
341
                // check to see if there is a dynamic subject
342 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...
343
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name);
344
345
                    if ($submittedFormField && trim($submittedFormField->Value)) {
346
                        $email->setSubject($submittedFormField->Value);
347
                    }
348
                }
349
350
                $this->extend('updateEmail', $email, $recipient, $emailData);
351
352
                if ($recipient->SendPlain) {
353
                    $body = strip_tags($recipient->getEmailBodyContent()) . "\n";
354
                    if (isset($emailData['Fields']) && !$recipient->HideFormData) {
355
                        foreach ($emailData['Fields'] as $Field) {
356
                            $body .= $Field->Title . ': ' . $Field->Value . " \n";
357
                        }
358
                    }
359
360
                    $email->setBody($body);
361
                    $email->sendPlain();
362
                } else {
363
                    $email->send();
364
                }
365
            }
366
        }
367
368
        $submittedForm->extend('updateAfterProcess');
369
370
        $session = $this->getRequest()->getSession();
371
        $session->clear("FormInfo.{$form->FormName()}.errors");
372
        $session->clear("FormInfo.{$form->FormName()}.data");
373
374
        $referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : "";
375
376
        // set a session variable from the security ID to stop people accessing
377
        // the finished method directly.
378
        if (!$this->DisableAuthenicatedFinishAction) {
379
            if (isset($data['SecurityID'])) {
380
                $session->set('FormProcessed', $data['SecurityID']);
381
            } else {
382
                // if the form has had tokens disabled we still need to set FormProcessed
383
                // to allow us to get through the finshed method
384
                if (!$this->Form()->getSecurityToken()->isEnabled()) {
385
                    $randNum = rand(1, 1000);
386
                    $randHash = md5($randNum);
387
                    $session->set('FormProcessed', $randHash);
388
                    $session->set('FormProcessedNum', $randNum);
389
                }
390
            }
391
        }
392
393
        if (!$this->DisableSaveSubmissions) {
394
            $session->set('userformssubmission'. $this->ID, $submittedForm->ID);
395
        }
396
397
        return $this->redirect($this->Link('finished') . $referrer . $this->config()->get('finished_anchor'));
398
    }
399
400
    /**
401
     * Allows the use of field values in email body.
402
     *
403
     * @param ArrayList $fields
404
     * @return ArrayData
405
     */
406
    protected function getMergeFieldsMap($fields = [])
407
    {
408
        $data = ArrayData::create([]);
409
410
        foreach ($fields as $field) {
411
            $data->setField($field->Name, DBField::create_field('Text', $field->Value));
412
        }
413
414
        return $data;
415
    }
416
417
    /**
418
     * This action handles rendering the "finished" message, which is
419
     * customizable by editing the ReceivedFormSubmission template.
420
     *
421
     * @return ViewableData
422
     */
423
    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...
424
    {
425
        $submission = $this->getRequest()->getSession()->get('userformssubmission'. $this->ID);
426
427
        if ($submission) {
428
            $submission = SubmittedForm::get()->byId($submission);
429
        }
430
431
        $referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null;
432
433
        if (!$this->DisableAuthenicatedFinishAction) {
434
            $formProcessed = $this->getRequest()->getSession()->get('FormProcessed');
435
436
            if (!isset($formProcessed)) {
437
                return $this->redirect($this->Link() . $referrer);
438
            } else {
439
                $securityID = $this->getRequest()->getSession()->get('SecurityID');
440
                // make sure the session matches the SecurityID and is not left over from another form
441
                if ($formProcessed != $securityID) {
442
                    // they may have disabled tokens on the form
443
                    $securityID = md5($this->getRequest()->getSession()->get('FormProcessedNum'));
444
                    if ($formProcessed != $securityID) {
445
                        return $this->redirect($this->Link() . $referrer);
446
                    }
447
                }
448
            }
449
450
            $this->getRequest()->getSession()->clear('FormProcessed');
451
        }
452
453
        $data = [
454
            'Submission' => $submission,
455
            'Link' => $referrer
456
        ];
457
458
        $this->extend('updateReceivedFormSubmissionData', $data);
459
460
        return $this->customise([
461
            'Content' => $this->customise($data)->renderWith(__CLASS__ . '_ReceivedFormSubmission'),
462
            'Form' => '',
463
        ]);
464
    }
465
466
    /**
467
     * Outputs the required JS from the $watch input
468
     *
469
     * @param array $watch
470
     *
471
     * @return string
472
     */
473
    protected function buildWatchJS($watch)
474
    {
475
        $result = '';
476
        foreach ($watch as $key => $rule) {
477
            $events = implode(' ', $rule['events']);
478
            $selectors = implode(', ', $rule['selectors']);
479
            $conjunction = $rule['conjunction'];
480
            $operations = implode(" {$conjunction} ", $rule['operations']);
481
            $target = $rule['targetFieldID'];
482
483
            $result .= <<<EOS
484
\n
485
    $('.userform').on('{$events}',
486
    "{$selectors}",
487
    function (){
488
        if ({$operations}) {
489
            $('{$target}').{$rule['view']};
490
        } else {
491
            $('{$target}').{$rule['opposite']};
492
        }
493
    });
494
    $("{$target}").find('.hide').removeClass('hide');
495
EOS;
496
        }
497
498
        return $result;
499
    }
500
}
501