Completed
Push — master ( 84eecb...d16692 )
by Damian
16:50
created

UserDefinedForm   C

Complexity

Total Complexity 17

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 30

Test Coverage

Coverage 82.11%

Importance

Changes 0
Metric Value
wmc 17
lcom 0
cbo 30
dl 0
loc 356
ccs 101
cts 123
cp 0.8211
rs 5
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
D getCMSFields() 0 142 9
A FilteredEmailRecipients() 0 13 1
A getFormOptions() 0 20 3
A getErrorContainerID() 0 4 1
A requireDefaultRecords() 0 16 2
A getCMSValidator() 0 4 1
1
<?php
2
3
/**
4
 * @package userforms
5
 */
6
7
class UserDefinedForm extends Page
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
8
{
9
10
    /**
11
     * @var string
12
     */
13
    private static $icon = 'userforms/images/sitetree_icon.png';
0 ignored issues
show
Unused Code introduced by
The property $icon 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...
14
15
    /**
16
     * @var string
17
     */
18
    private static $description = 'Adds a customizable form.';
0 ignored issues
show
Unused Code introduced by
The property $description 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...
19
20
    /**
21
     * @var string Required Identifier
22
     */
23
    private static $required_identifier = null;
0 ignored issues
show
Unused Code introduced by
The property $required_identifier 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...
24
25
    /**
26
     * @var string
27
     */
28
    private static $email_template_directory = 'userforms/templates/email/';
0 ignored issues
show
Unused Code introduced by
The property $email_template_directory 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...
29
30
    /**
31
     * Should this module automatically upgrade on dev/build?
32
     *
33
     * @config
34
     * @var bool
35
     */
36
    private static $upgrade_on_build = true;
0 ignored issues
show
Unused Code introduced by
The property $upgrade_on_build 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...
37
38
    /**
39
     * Set this to true to disable automatic inclusion of CSS files
40
     * @config
41
     * @var bool
42
     */
43
    private static $block_default_userforms_css = false;
0 ignored issues
show
Unused Code introduced by
The property $block_default_userforms_css 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...
44
45
    /**
46
     * Set this to true to disable automatic inclusion of JavaScript files
47
     * @config
48
     * @var bool
49
     */
50
    private static $block_default_userforms_js = false;
0 ignored issues
show
Unused Code introduced by
The property $block_default_userforms_js 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...
51
52
    /**
53
     * Built in extensions required by this page
54
     * @config
55
     * @var array
56
     */
57
    private static $extensions = array(
0 ignored issues
show
Unused Code introduced by
The property $extensions 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...
58
        'UserFormFieldEditorExtension'
59
    );
60
61
    /**
62
     * @var array Fields on the user defined form page.
63
     */
64
    private static $db = array(
0 ignored issues
show
Unused Code introduced by
The property $db 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...
65
        "SubmitButtonText" => "Varchar",
66
        "ClearButtonText" => "Varchar",
67
        "OnCompleteMessage" => "HTMLText",
68
        "ShowClearButton" => "Boolean",
69
        'DisableSaveSubmissions' => 'Boolean',
70
        'EnableLiveValidation' => 'Boolean',
71
        'HideFieldLabels' => 'Boolean',
72
        'DisplayErrorMessagesAtTop' => 'Boolean',
73
        'DisableAuthenicatedFinishAction' => 'Boolean',
74
        'DisableCsrfSecurityToken' => 'Boolean'
75
    );
76
77
    /**
78
     * @var array Default values of variables when this page is created
79
     */
80
    private static $defaults = array(
0 ignored issues
show
Unused Code introduced by
The property $defaults 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...
81
        'Content' => '$UserDefinedForm',
82
        'DisableSaveSubmissions' => 0,
83
        'OnCompleteMessage' => '<p>Thanks, we\'ve received your submission.</p>'
84
    );
85
86
    /**
87
     * @var array
88
     */
89
    private static $has_many = array(
0 ignored issues
show
Unused Code introduced by
The property $has_many 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...
90
        "Submissions" => "SubmittedForm",
91
        "EmailRecipients" => "UserDefinedForm_EmailRecipient"
92
    );
93
94
    /**
95
     * @var array
96
     * @config
97
     */
98
    private static $casting = array(
0 ignored issues
show
Unused Code introduced by
The property $casting 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...
99
        'ErrorContainerID' => 'Text'
100
    );
101
102
    /**
103
     * Error container selector which matches the element for grouped messages
104
     *
105
     * @var string
106
     * @config
107
     */
108
    private static $error_container_id = 'error-container';
0 ignored issues
show
Unused Code introduced by
The property $error_container_id 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...
109
110
    /**
111
     * The configuration used to determine whether a confirmation message is to
112
     * appear when navigating away from a partially completed form.
113
     *
114
     * @var boolean
115
     * @config
116
     */
117
    private static $enable_are_you_sure = true;
0 ignored issues
show
Unused Code introduced by
The property $enable_are_you_sure 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...
118
119
    /**
120
     * @var bool
121
     * @config
122
     */
123
    private static $recipients_warning_enabled = false;
0 ignored issues
show
Unused Code introduced by
The property $recipients_warning_enabled 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...
124
125
    /**
126
     * Temporary storage of field ids when the form is duplicated.
127
     * Example layout: array('EditableCheckbox3' => 'EditableCheckbox14')
128
     * @var array
129
     */
130
    protected $fieldsFromTo = array();
131
132
    /**
133
     * @return FieldList
134
     */
135 2
     public function getCMSFields()
136
     {
137 2
         Requirements::css(USERFORMS_DIR . '/css/UserForm_cms.css');
138
139 2
         $self = $this;
140
141
         $this->beforeUpdateCMSFields(function ($fields) use ($self) {
142
143
            // define tabs
144 2
            $fields->findOrMakeTab('Root.FormOptions', _t('UserDefinedForm.CONFIGURATION', 'Configuration'));
145 2
            $fields->findOrMakeTab('Root.Recipients', _t('UserDefinedForm.RECIPIENTS', 'Recipients'));
146 2
            $fields->findOrMakeTab('Root.Submissions', _t('UserDefinedForm.SUBMISSIONS', 'Submissions'));
147
148
            // text to show on complete
149 2
            $onCompleteFieldSet = new CompositeField(
150 2
                $label = new LabelField('OnCompleteMessageLabel', _t('UserDefinedForm.ONCOMPLETELABEL', 'Show on completion')),
151 2
                $editor = new HtmlEditorField('OnCompleteMessage', '', _t('UserDefinedForm.ONCOMPLETEMESSAGE', $self->OnCompleteMessage))
152 2
            );
153
154 2
            $onCompleteFieldSet->addExtraClass('field');
155
156 2
            $editor->setRows(3);
157 2
            $label->addExtraClass('left');
158
159
            // Define config for email recipients
160 2
            $emailRecipientsConfig = GridFieldConfig_RecordEditor::create(10);
161 2
            $emailRecipientsConfig->getComponentByType('GridFieldAddNewButton')
162 2
                ->setButtonName(
163 2
                    _t('UserDefinedForm.ADDEMAILRECIPIENT', 'Add Email Recipient')
164 2
                );
165
166
            // who do we email on submission
167 2
            $emailRecipients = new GridField(
168 2
                'EmailRecipients',
169 2
                _t('UserDefinedForm.EMAILRECIPIENTS', 'Email Recipients'),
170 2
                $self->EmailRecipients(),
171
                $emailRecipientsConfig
172 2
            );
173
            $emailRecipients
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setItemRequestClass() does only exist in the following implementations of said interface: GridFieldAddNewMultiClass, GridFieldDetailForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
174 2
                ->getConfig()
175 2
                ->getComponentByType('GridFieldDetailForm')
176 2
                ->setItemRequestClass('UserFormRecipientItemRequest');
177
178 2
            $fields->addFieldsToTab('Root.FormOptions', $onCompleteFieldSet);
179 2
            $fields->addFieldToTab('Root.Recipients', $emailRecipients);
180 2
            $fields->addFieldsToTab('Root.FormOptions', $self->getFormOptions());
181
182
183
            // view the submissions
184 2
            $submissions = new GridField(
185 2
                'Submissions',
186 2
                _t('UserDefinedForm.SUBMISSIONS', 'Submissions'),
187 2
                 $self->Submissions()->sort('Created', 'DESC')
188 2
            );
189
190
            // make sure a numeric not a empty string is checked against this int column for SQL server
191 2
            $parentID = (!empty($self->ID)) ? (int) $self->ID : 0;
192
193
            // get a list of all field names and values used for print and export CSV views of the GridField below.
194
            $columnSQL = <<<SQL
195
SELECT "SubmittedFormField"."Name" as "Name", COALESCE("EditableFormField"."Title", "SubmittedFormField"."Title") as "Title", COALESCE("EditableFormField"."Sort", 999) AS "Sort"
196
FROM "SubmittedFormField"
197
LEFT JOIN "SubmittedForm" ON "SubmittedForm"."ID" = "SubmittedFormField"."ParentID"
198
LEFT JOIN "EditableFormField" ON "EditableFormField"."Name" = "SubmittedFormField"."Name" AND "EditableFormField"."ParentID" = '$parentID'
199 2
WHERE "SubmittedForm"."ParentID" = '$parentID'
200 2
ORDER BY "Sort", "Title"
201 2
SQL;
202
            // Sanitise periods in title
203 2
            $columns = array();
204 2
            foreach (DB::query($columnSQL)->map() as $name => $title) {
205
                $columns[$name] = trim(strtr($title, '.', ' '));
206 2
            }
207
208 2
            $config = new GridFieldConfig();
209 2
            $config->addComponent(new GridFieldToolbarHeader());
210 2
            $config->addComponent($sort = new GridFieldSortableHeader());
211 2
            $config->addComponent($filter = new UserFormsGridFieldFilterHeader());
212 2
            $config->addComponent(new GridFieldDataColumns());
213 2
            $config->addComponent(new GridFieldEditButton());
214 2
            $config->addComponent(new GridFieldDeleteAction());
215 2
            $config->addComponent(new GridFieldPageCount('toolbar-header-right'));
216 2
            $config->addComponent($pagination = new GridFieldPaginator(25));
217 2
            $config->addComponent(new GridFieldDetailForm());
218 2
            $config->addComponent(new GridFieldButtonRow('after'));
219 2
            $config->addComponent($export = new GridFieldExportButton('buttons-after-left'));
220 2
            $config->addComponent($print = new GridFieldPrintButton('buttons-after-left'));
221
222
            // show user form items in the summary tab
223
            $summaryarray = array(
224 2
                'ID' => 'ID',
225 2
                'Created' => 'Created',
226
                'LastEdited' => 'Last Edited'
227 2
            );
228 2
            foreach(EditableFormField::get()->filter(array("ParentID" => $parentID)) as $eff) {
229 2
                if($eff->ShowInSummary) {
230 1
                    $summaryarray[$eff->Name] = $eff->Title ?: $eff->Name;
231 1
                }
232 2
            }
233
234 2
            $config->getComponentByType('GridFieldDataColumns')->setDisplayFields($summaryarray);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns, GridFieldEditableColumns, GridFieldExternalLink.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
235
236
            /**
237
             * Support for {@link https://github.com/colymba/GridFieldBulkEditingTools}
238
             */
239 2
            if (class_exists('GridFieldBulkManager')) {
240
                $config->addComponent(new GridFieldBulkManager());
241
            }
242
243 2
            $sort->setThrowExceptionOnBadDataType(false);
244 2
            $filter->setThrowExceptionOnBadDataType(false);
245 2
            $pagination->setThrowExceptionOnBadDataType(false);
246
247
            // attach every column to the print view form
248 2
            $columns['Created'] = 'Created';
249 2
            $filter->setColumns($columns);
250
251
            // print configuration
252
253 2
            $print->setPrintHasHeader(true);
254 2
            $print->setPrintColumns($columns);
255
256
            // export configuration
257 2
            $export->setCsvHasHeader(true);
258 2
            $export->setExportColumns($columns);
259
260 2
            $submissions->setConfig($config);
261 2
            $fields->addFieldToTab('Root.Submissions', $submissions);
262 2
            $fields->addFieldToTab('Root.FormOptions', new CheckboxField('DisableSaveSubmissions', _t('UserDefinedForm.SAVESUBMISSIONS', 'Disable Saving Submissions to Server')));
263
264 2
        });
265
266 2
         $fields = parent::getCMSFields();
267
268 2
         if ($this->EmailRecipients()->Count() == 0 && static::config()->recipients_warning_enabled) {
269
             $fields->addFieldToTab("Root.Main", new LiteralField("EmailRecipientsWarning",
270
                "<p class=\"message warning\">" . _t("UserDefinedForm.NORECIPIENTS",
271
                "Warning: You have not configured any recipients. Form submissions may be missed.")
272
                . "</p>"), "Title");
273
         }
274
275 2
         return $fields;
276
     }
277
278
    /**
279
     * Allow overriding the EmailRecipients on a {@link DataExtension}
280
     * so you can customise who receives an email.
281
     * Converts the RelationList to an ArrayList so that manipulation
282
     * of the original source data isn't possible.
283
     *
284
     * @return ArrayList
285
     */
286 3
    public function FilteredEmailRecipients($data = null, $form = null)
287
    {
288 3
        $recipients = new ArrayList($this->EmailRecipients()->toArray());
289
290
        // Filter by rules
291
        $recipients = $recipients->filterByCallback(function ($recipient) use ($data, $form) {
292 2
            return $recipient->canSend($data, $form);
293 3
        });
294
295 3
        $this->extend('updateFilteredEmailRecipients', $recipients, $data, $form);
296
297 3
        return $recipients;
298
    }
299
300
    /**
301
     * Custom options for the form. You can extend the built in options by
302
     * using {@link updateFormOptions()}
303
     *
304
     * @return FieldList
305
     */
306 3
    public function getFormOptions()
307
    {
308 3
        $submit = ($this->SubmitButtonText) ? $this->SubmitButtonText : _t('UserDefinedForm.SUBMITBUTTON', 'Submit');
309 3
        $clear = ($this->ClearButtonText) ? $this->ClearButtonText : _t('UserDefinedForm.CLEARBUTTON', 'Clear');
310
311 3
        $options = new FieldList(
312 3
            new TextField("SubmitButtonText", _t('UserDefinedForm.TEXTONSUBMIT', 'Text on submit button:'), $submit),
313 3
            new TextField("ClearButtonText", _t('UserDefinedForm.TEXTONCLEAR', 'Text on clear button:'), $clear),
314 3
            new CheckboxField("ShowClearButton", _t('UserDefinedForm.SHOWCLEARFORM', 'Show Clear Form Button'), $this->ShowClearButton),
315 3
            new CheckboxField("EnableLiveValidation", _t('UserDefinedForm.ENABLELIVEVALIDATION', 'Enable live validation')),
316 3
            new CheckboxField("HideFieldLabels", _t('UserDefinedForm.HIDEFIELDLABELS', 'Hide field labels')),
317 3
            new CheckboxField("DisplayErrorMessagesAtTop", _t('UserDefinedForm.DISPLAYERRORMESSAGESATTOP', 'Display error messages above the form?')),
318 3
            new CheckboxField('DisableCsrfSecurityToken', _t('UserDefinedForm.DISABLECSRFSECURITYTOKEN', 'Disable CSRF Token')),
319 3
            new CheckboxField('DisableAuthenicatedFinishAction', _t('UserDefinedForm.DISABLEAUTHENICATEDFINISHACTION', 'Disable Authentication on finish action'))
320 3
        );
321
322 3
        $this->extend('updateFormOptions', $options);
323
324 3
        return $options;
325
    }
326
327
    /**
328
     * Get the HTML id of the error container displayed above the form.
329
     *
330
     * @return string
331
     */
332
    public function getErrorContainerID()
333
    {
334
        return $this->config()->error_container_id;
335
    }
336
337
    public function requireDefaultRecords()
338
    {
339
        parent::requireDefaultRecords();
340
341
        if (!$this->config()->upgrade_on_build) {
342
            return;
343
        }
344
345
        // Perform migrations
346
        Injector::inst()
347
            ->create('UserFormsUpgradeService')
348
            ->setQuiet(true)
349
            ->run();
350
351
        DB::alteration_message('Migrated userforms', 'changed');
352
    }
353
354
355
    /**
356
     * Validate formfields
357
     */
358
    public function getCMSValidator()
359
    {
360
        return new UserFormValidator();
361
    }
362
}
363
364
/**
365
 * Controller for the {@link UserDefinedForm} page type.
366
 *
367
 * @package userforms
368
 */
369
370
class UserDefinedForm_Controller extends Page_Controller
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
371
{
372
373
    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...
374
375
    private static $allowed_actions = array(
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...
376
        'index',
377
        'ping',
378
        'Form',
379
        'finished'
380
    );
381
382 6
    public function init()
383
    {
384 6
        parent::init();
385
386 5
        $page = $this->data();
387
388
        // load the css
389 5
        if (!$page->config()->block_default_userforms_css) {
390 5
            Requirements::css(USERFORMS_DIR . '/css/UserForm.css');
391 5
        }
392
393
        // load the jquery
394 5
        if (!$page->config()->block_default_userforms_js) {
395 5
            $lang = i18n::get_lang_from_locale(i18n::get_locale());
396 5
            Requirements::javascript(FRAMEWORK_DIR .'/thirdparty/jquery/jquery.js');
397 5
            Requirements::javascript(USERFORMS_DIR . '/thirdparty/jquery-validate/jquery.validate.min.js');
398 5
            Requirements::add_i18n_javascript(USERFORMS_DIR . '/javascript/lang');
399 5
            Requirements::javascript(USERFORMS_DIR . '/javascript/UserForm.js');
400
401 5
            Requirements::javascript(
402 5
                USERFORMS_DIR . "/thirdparty/jquery-validate/localization/messages_{$lang}.min.js"
403 5
            );
404 5
            Requirements::javascript(
405 5
                USERFORMS_DIR . "/thirdparty/jquery-validate/localization/methods_{$lang}.min.js"
406 5
            );
407 5
            if ($this->HideFieldLabels) {
408 1
                Requirements::javascript(USERFORMS_DIR . '/thirdparty/Placeholders.js/Placeholders.min.js');
409
            }
410
411
            // Bind a confirmation message when navigating away from a partially completed form.
412 5
            if ($page::config()->enable_are_you_sure) {
413 5
                Requirements::javascript(USERFORMS_DIR . '/thirdparty/jquery.are-you-sure/jquery.are-you-sure.js');
414 5
            }
415 5
        }
416 5
    }
417
418
    /**
419
     * Using $UserDefinedForm in the Content area of the page shows
420
     * where the form should be rendered into. If it does not exist
421
     * then default back to $Form.
422
     *
423
     * @return array
424
     */
425 6
    public function index()
426
    {
427 6
        if ($this->Content && $form = $this->Form()) {
428 6
            $hasLocation = stristr($this->Content, '$UserDefinedForm');
429 6
            if ($hasLocation) {
430 5
                $content = preg_replace('/(<p[^>]*>)?\\$UserDefinedForm(<\\/p>)?/i', $form->forTemplate(), $this->Content);
431
                return array(
432 5
                    'Content' => DBField::create_field('HTMLText', $content),
433
                    'Form' => ""
434 5
                );
435
            }
436 1
        }
437
438
        return array(
439 1
            'Content' => DBField::create_field('HTMLText', $this->Content),
440 1
            'Form' => $this->Form()
441 1
        );
442
    }
443
444
    /**
445
     * Keep the session alive for the user.
446
     *
447
     * @return int
448
     */
449
    public function ping()
450
    {
451
        return 1;
452
    }
453
454
    /**
455
     * Get the form for the page. Form can be modified by calling {@link updateForm()}
456
     * on a UserDefinedForm extension.
457
     *
458
     * @return Forms
459
     */
460 9
    public function Form()
461
    {
462 9
        $form = UserForm::create($this);
463 9
        $this->generateConditionalJavascript();
464 9
        return $form;
465
    }
466
467
    /**
468
     * Generate the javascript for the conditional field show / hiding logic.
469
     *
470
     * @return void
471
     */
472 9
    public function generateConditionalJavascript()
473
    {
474 9
        $default = "";
475 9
        $rules = "";
476
477 9
        $watch = array();
478 9
        $watchLoad = array();
479
480 9
        if ($this->Fields()) {
481 9
            foreach ($this->Fields() as $field) {
482 8
                $holderSelector = $field->getSelectorHolder();
483
484
                // Is this Field Show by Default
485 8
                if (!$field->ShowOnLoad) {
486
                    $default .= "{$holderSelector}.hide().trigger('userform.field.hide');\n";
487
                }
488
489
                // Check for field dependencies / default
490 8
                foreach ($field->EffectiveDisplayRules() as $rule) {
491
492
                    // Get the field which is effected
493
                    $formFieldWatch = EditableFormField::get()->byId($rule->ConditionFieldID);
494
495
                    // Skip deleted fields
496
                    if (!$formFieldWatch) {
497
                        continue;
498
                    }
499
500
                    $fieldToWatch = $formFieldWatch->getSelectorField($rule);
501
                    $fieldToWatchOnLoad = $formFieldWatch->getSelectorField($rule, true);
502
503
                    // show or hide?
504
                    $view = ($rule->Display == 'Hide') ? 'hide' : 'show';
505
                    $opposite = ($view == "show") ? "hide" : "show";
506
507
                    // what action do we need to keep track of. Something nicer here maybe?
508
                    // @todo encapulsation
509
                    $action = "change";
510
511
                    if ($formFieldWatch instanceof EditableTextField) {
512
                        $action = "keyup";
513
                    }
514
515
                    // is this field a special option field
516
                    $checkboxField = false;
517
                    $radioField = false;
518
519
                    if (in_array($formFieldWatch->ClassName, array('EditableCheckboxGroupField', 'EditableCheckbox'))) {
520
                        $action = "click";
521
                        $checkboxField = true;
522
                    } elseif ($formFieldWatch->ClassName == "EditableRadioField") {
523
                        $radioField = true;
524
                    }
525
526
                    // and what should we evaluate
527
                    switch ($rule->ConditionOption) {
528
                        case 'IsNotBlank':
529
                            $expression = ($checkboxField || $radioField) ? '$(this).is(":checked")' :'$(this).val() != ""';
530
531
                            break;
532
                        case 'IsBlank':
533
                            $expression = ($checkboxField || $radioField) ? '!($(this).is(":checked"))' : '$(this).val() == ""';
534
535 1
                            break;
536 View Code Duplication
                        case 'HasValue':
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...
537
                            if ($checkboxField) {
538
                                $expression = '$(this).prop("checked")';
539
                            } elseif ($radioField) {
540
                                // We cannot simply get the value of the radio group, we need to find the checked option first.
541
                                $expression = '$(this).closest(".field, .control-group").find("input:checked").val()=="'. $rule->FieldValue .'"';
542
                            } else {
543
                                $expression = '$(this).val() == "'. $rule->FieldValue .'"';
544
                            }
545
546
                            break;
547
                        case 'ValueLessThan':
548 1
                            $expression = '$(this).val() < parseFloat("'. $rule->FieldValue .'")';
549
550
                            break;
551
                        case 'ValueLessThanEqual':
552
                            $expression = '$(this).val() <= parseFloat("'. $rule->FieldValue .'")';
553
554
                            break;
555
                        case 'ValueGreaterThan':
556 1
                            $expression = '$(this).val() > parseFloat("'. $rule->FieldValue .'")';
557
558
                            break;
559
                        case 'ValueGreaterThanEqual':
560
                            $expression = '$(this).val() >= parseFloat("'. $rule->FieldValue .'")';
561
562
                            break;
563 View Code Duplication
                        default: // ==HasNotValue
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...
564 1
                            if ($checkboxField) {
565
                                $expression = '!$(this).prop("checked")';
566
                            } elseif ($radioField) {
567
                                // We cannot simply get the value of the radio group, we need to find the checked option first.
568
                                $expression = '$(this).parents(".field, .control-group").find("input:checked").val()!="'. $rule->FieldValue .'"';
569
                            } else {
570
                                $expression = '$(this).val() != "'. $rule->FieldValue .'"';
571
                            }
572
573
                            break;
574
                    }
575
576
                    if (!isset($watch[$fieldToWatch])) {
577
                        $watch[$fieldToWatch] = array();
578
                    }
579
580
                    $watch[$fieldToWatch][] = array(
581
                        'expression' => $expression,
582
                        'holder_selector' => $holderSelector,
583
                        'view' => $view,
584
                        'opposite' => $opposite,
585
                        'action' => $action
586
                    );
587
588
                    $watchLoad[$fieldToWatchOnLoad] = true;
589 8
                }
590 9
            }
591 9
        }
592
593 9
        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...
594
            foreach ($watch as $key => $values) {
595
                $logic = array();
596
                $actions = array();
597
598
                foreach ($values as $rule) {
599
                    // Assign action
600
                    $actions[$rule['action']] = $rule['action'];
601
602
                    // Assign behaviour
603
                    $expression = $rule['expression'];
604
                    $holder = $rule['holder_selector'];
605
                    $view = $rule['view']; // hide or show
606
                    $opposite = $rule['opposite'];
607
                    // Generated javascript for triggering visibility
608
                    $logic[] = <<<"EOS"
609
if({$expression}) {
610
	{$holder}
611
		.{$view}()
612
		.trigger('userform.field.{$view}');
613
} else {
614
	{$holder}
615
		.{$opposite}()
616 1
		.trigger('userform.field.{$opposite}');
617
}
618
EOS;
619
                }
620
621
                $logic = implode("\n", $logic);
622
                $rules .= $key.".each(function() {\n
623
	$(this).data('userformConditions', function() {\n
624 1
		$logic\n
625
	}); \n
626 3
});\n";
627
                foreach ($actions as $action) {
628
                    $rules .= $key.".$action(function() {
629
	$(this).data('userformConditions').call(this);\n
630
});\n";
631
                }
632 1
            }
633
        }
634
635 9
        if ($watchLoad) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $watchLoad 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...
636
            foreach ($watchLoad as $key => $value) {
637
                $rules .= $key.".each(function() {
638
	$(this).data('userformConditions').call(this);\n
639
});\n";
640
            }
641
        }
642
643
        // Only add customScript if $default or $rules is defined
644 9
        if ($default  || $rules) {
645
            Requirements::customScript(<<<JS
646
				(function($) {
647
					$(document).ready(function() {
648
						$default
649
650
						$rules
651
					})
652
				})(jQuery);
653
JS
654
, 'UserFormsConditional');
655
        }
656 9
    }
657
658
    /**
659
     * Process the form that is submitted through the site
660
     *
661
     * {@see UserForm::validate()} for validation step prior to processing
662
     *
663
     * @param array $data
664
     * @param Form $form
665
     *
666
     * @return Redirection
667
     */
668 3
    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...
669 3
    {
670 2
        $submittedForm = Object::create('SubmittedForm');
671 2
        $submittedForm->SubmittedByID = ($id = Member::currentUserID()) ? $id : 0;
672 2
        $submittedForm->ParentID = $this->ID;
673
674
        // if saving is not disabled save now to generate the ID
675 2
        if (!$this->DisableSaveSubmissions) {
676 2
            $submittedForm->write();
677 2
        }
678
679 2
        $attachments = array();
680 2
        $submittedFields = new ArrayList();
681
682 2
        foreach ($this->Fields() as $field) {
683 2
            if (!$field->showInReports()) {
684 2
                continue;
685
            }
686
687 2
            $submittedField = $field->getSubmittedFormField();
688 2
            $submittedField->ParentID = $submittedForm->ID;
689 2
            $submittedField->Name = $field->Name;
690 2
            $submittedField->Title = $field->getField('Title');
691
692
            // save the value from the data
693 2
            if ($field->hasMethod('getValueFromData')) {
694
                $submittedField->Value = $field->getValueFromData($data);
695
            } else {
696 2
                if (isset($data[$field->Name])) {
697 2
                    $submittedField->Value = $data[$field->Name];
698 2
                }
699
            }
700
701 2
            if (!empty($data[$field->Name])) {
702 2
                if (in_array("EditableFileField", $field->getClassAncestry())) {
703
                    if (!empty($_FILES[$field->Name]['name'])) {
704
                        $foldername = $field->getFormField()->getFolderName();
705
706
                        // create the file from post data
707
                        $upload = new Upload();
708
                        $file = new File();
709
                        $file->ShowInSearch = 0;
710
                        try {
711
                            $upload->loadIntoFile($_FILES[$field->Name], $file, $foldername);
712
                        } catch (ValidationException $e) {
713
                            $validationResult = $e->getResult();
714
                            $form->addErrorMessage($field->Name, $validationResult->message(), 'bad');
715
                            Controller::curr()->redirectBack();
716
                            return;
717
                        }
718
719
                        // write file to form field
720
                        $submittedField->UploadedFileID = $file->ID;
721
722
                        // attach a file only if lower than 1MB
723
                        if ($file->getAbsoluteSize() < 1024*1024*1) {
724
                            $attachments[] = $file;
725
                        }
726
                    }
727
                }
728 2
            }
729
730 2
            $submittedField->extend('onPopulationFromField', $field);
731
732 2
            if (!$this->DisableSaveSubmissions) {
733 2
                $submittedField->write();
734 2
            }
735
736 2
            $submittedFields->push($submittedField);
737 2
        }
738
739
        $emailData = array(
740 2
            "Sender" => Member::currentUser(),
741
            "Fields" => $submittedFields
742 2
        );
743
744 2
        $this->extend('updateEmailData', $emailData, $attachments);
745
746
        // email users on submit.
747 2
        if ($recipients = $this->FilteredEmailRecipients($data, $form)) {
748 2
            foreach ($recipients as $recipient) {
749 1
                $email = new UserFormRecipientEmail($submittedFields);
750 1
                $mergeFields = $this->getMergeFieldsMap($emailData['Fields']);
751
752 1
                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...
753
                    foreach ($attachments as $file) {
754
                        if ($file->ID != 0) {
755
                            $email->attachFile(
756
                                $file->Filename,
757
                                $file->Filename,
758
                                HTTP::get_mime_type($file->Filename)
759
                            );
760
                        }
761
                    }
762
                }
763
764 1
                $parsedBody = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields);
765
766 1
                if (!$recipient->SendPlain && $recipient->emailTemplateExists()) {
767
                    $email->setTemplate($recipient->EmailTemplate);
768
                }
769
770 1
                $email->populateTemplate($recipient);
771 1
                $email->populateTemplate($emailData);
772 1
                $email->setFrom($recipient->EmailFrom);
773 1
                $email->setBody($parsedBody);
774 1
                $email->setTo($recipient->EmailAddress);
775 1
                $email->setSubject($recipient->EmailSubject);
776
777 1
                if ($recipient->EmailReplyTo) {
778
                    $email->setReplyTo($recipient->EmailReplyTo);
779
                }
780
781
                // check to see if they are a dynamic reply to. eg based on a email field a user selected
782 1 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...
783 1
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name);
784
785 1
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
786
                        $email->setReplyTo($submittedFormField->Value);
787
                    }
788 1
                }
789
                // check to see if they are a dynamic reciever eg based on a dropdown field a user selected
790 1 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...
791 1
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name);
792
793 1
                    if ($submittedFormField && is_string($submittedFormField->Value)) {
794
                        $email->setTo($submittedFormField->Value);
795
                    }
796 1
                }
797
798
                // check to see if there is a dynamic subject
799 1 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...
800 1
                    $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name);
801
802 1
                    if ($submittedFormField && trim($submittedFormField->Value)) {
803
                        $email->setSubject($submittedFormField->Value);
804
                    }
805 1
                }
806
807 1
                $this->extend('updateEmail', $email, $recipient, $emailData);
808
809 1
                if ($recipient->SendPlain) {
810 1
                    $body = strip_tags($recipient->getEmailBodyContent()) . "\n";
811 1
                    if (isset($emailData['Fields']) && !$recipient->HideFormData) {
812 1
                        foreach ($emailData['Fields'] as $Field) {
813 1
                            $body .= $Field->Title .': '. $Field->Value ." \n";
814 1
                        }
815 1
                    }
816
817 1
                    $email->setBody($body);
818 1
                    $email->sendPlain();
819 1
                } else {
820 1
                    $email->send();
821
                }
822 2
            }
823 2
        }
824
825 2
        $submittedForm->extend('updateAfterProcess');
826
827 2
        Session::clear("FormInfo.{$form->FormName()}.errors");
828 2
        Session::clear("FormInfo.{$form->FormName()}.data");
829
830 2
        $referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : "";
831
832
        // set a session variable from the security ID to stop people accessing
833
        // the finished method directly.
834 2
        if (!$this->DisableAuthenicatedFinishAction) {
835 2
            if (isset($data['SecurityID'])) {
836
                Session::set('FormProcessed', $data['SecurityID']);
837
            } else {
838
                // if the form has had tokens disabled we still need to set FormProcessed
839
                // to allow us to get through the finshed method
840 2
                if (!$this->Form()->getSecurityToken()->isEnabled()) {
841 2
                    $randNum = rand(1, 1000);
842 2
                    $randHash = md5($randNum);
843 2
                    Session::set('FormProcessed', $randHash);
844 2
                    Session::set('FormProcessedNum', $randNum);
845 2
                }
846
            }
847 2
        }
848
849 2
        if (!$this->DisableSaveSubmissions) {
850 2
            Session::set('userformssubmission'. $this->ID, $submittedForm->ID);
851 2
        }
852
853 2
        return $this->redirect($this->Link('finished') . $referrer . $this->config()->finished_anchor);
854
    }
855
856
    /**
857
     * Allows the use of field values in email body.
858
     *
859
     * @param ArrayList fields
860
     * @return ArrayData
861
     */
862 1
    private function getMergeFieldsMap($fields = array())
863
    {
864 1
        $data = new ArrayData(array());
865
866 1
        foreach ($fields as $field) {
867 1
            $data->setField($field->Name, DBField::create_field('Text', $field->Value));
868 1
        }
869
870 1
        return $data;
871
    }
872
873
    /**
874
     * This action handles rendering the "finished" message, which is
875
     * customizable by editing the ReceivedFormSubmission template.
876
     *
877
     * @return ViewableData
878
     */
879 8
    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...
880
    {
881 3
        $submission = Session::get('userformssubmission'. $this->ID);
882
883 3
        if ($submission) {
884 1
            $submission = SubmittedForm::get()->byId($submission);
885 1
        }
886
887 3
        $referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null;
888
889 3
        if (!$this->DisableAuthenicatedFinishAction) {
890 3
            $formProcessed = Session::get('FormProcessed');
891
892 3
            if (!isset($formProcessed)) {
893 1
                return $this->redirect($this->Link() . $referrer);
894
            } else {
895 2
                $securityID = Session::get('SecurityID');
896
                // make sure the session matches the SecurityID and is not left over from another form
897 2
                if ($formProcessed != $securityID) {
898
                    // they may have disabled tokens on the form
899 1
                    $securityID = md5(Session::get('FormProcessedNum'));
900 1
                    if ($formProcessed != $securityID) {
901
                        return $this->redirect($this->Link() . $referrer);
902 8
                    }
903 1
                }
904
            }
905
906 2
            Session::clear('FormProcessed');
907 2
        }
908
909 2
        return $this->customise(array(
910 2
            'Content' => $this->customise(array(
911 2
                'Submission' => $submission,
912
                'Link' => $referrer
913 2
            ))->renderWith('ReceivedFormSubmission'),
914 2
            'Form' => '',
915 2
        ));
916
    }
917
}
918