Completed
Pull Request — master (#429)
by Nathan
34:18
created

UserDefinedForm_Controller::init()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 27
rs 8.8571
cc 3
eloc 17
nc 4
nop 0
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
	 * @var string
11
	 */
12
	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...
13
14
	/**
15
	 * @var string Required Identifier
16
	 */
17
	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...
18
19
	/**
20
	 * @var string
21
	 */
22
	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...
23
24
	/**
25
	 * Should this module automatically upgrade on dev/build?
26
	 *
27
	 * @config
28
	 * @var bool
29
	 */
30
	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...
31
32
	/**
33
	 * Built in extensions required by this page
34
	 * @config
35
	 * @var array
36
	 */
37
	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...
38
		'UserFormFieldEditorExtension'
39
	);
40
41
	/**
42
	 * @var array Fields on the user defined form page.
43
	 */
44
	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...
45
		"SubmitButtonText" => "Varchar",
46
		"ClearButtonText" => "Varchar",
47
		"OnCompleteMessage" => "HTMLText",
48
		"ShowClearButton" => "Boolean",
49
		'DisableSaveSubmissions' => 'Boolean',
50
		'EnableLiveValidation' => 'Boolean',
51
		'HideFieldLabels' => 'Boolean',
52
		'DisplayErrorMessagesAtTop' => 'Boolean',
53
		'DisableAuthenicatedFinishAction' => 'Boolean',
54
		'DisableCsrfSecurityToken' => 'Boolean'
55
	);
56
57
	/**
58
	 * @var array Default values of variables when this page is created
59
	 */
60
	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...
61
		'Content' => '$UserDefinedForm',
62
		'DisableSaveSubmissions' => 0,
63
		'OnCompleteMessage' => '<p>Thanks, we\'ve received your submission.</p>'
64
	);
65
66
	/**
67
	 * @var array
68
	 */
69
	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...
70
		"Submissions" => "SubmittedForm",
71
		"EmailRecipients" => "UserDefinedForm_EmailRecipient"
72
	);
73
74
	/**
75
	 * @var array
76
	 * @config
77
	 */
78
	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...
79
		'ErrorContainerID' => 'Text'
80
	);
81
82
	/**
83
	 * Error container selector which matches the element for grouped messages
84
	 *
85
	 * @var string
86
	 * @config
87
	 */
88
	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...
89
90
	/**
91
	 * The configuration used to determine whether a confirmation message is to
92
	 * appear when navigating away from a partially completed form.
93
	 *
94
	 * @var boolean
95
	 * @config
96
	 */
97
	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...
98
99
	/**
100
	 * Temporary storage of field ids when the form is duplicated.
101
	 * Example layout: array('EditableCheckbox3' => 'EditableCheckbox14')
102
	 * @var array
103
	 */
104
	protected $fieldsFromTo = array();
105
106
	/**
107
	 * @return FieldList
108
	 */
109
	 public function getCMSFields() {
110
		Requirements::css(USERFORMS_DIR . '/css/UserForm_cms.css');
111
112
		$self = $this;
113
114
		$this->beforeUpdateCMSFields(function($fields) use ($self) {
115
116
			// define tabs
117
			$fields->findOrMakeTab('Root.FormOptions', _t('UserDefinedForm.CONFIGURATION', 'Configuration'));
118
			$fields->findOrMakeTab('Root.Recipients', _t('UserDefinedForm.RECIPIENTS', 'Recipients'));
119
			$fields->findOrMakeTab('Root.Submissions', _t('UserDefinedForm.SUBMISSIONS', 'Submissions'));
120
121
			// text to show on complete
122
			$onCompleteFieldSet = new CompositeField(
123
				$label = new LabelField('OnCompleteMessageLabel',_t('UserDefinedForm.ONCOMPLETELABEL', 'Show on completion')),
124
				$editor = new HtmlEditorField( 'OnCompleteMessage', '', _t('UserDefinedForm.ONCOMPLETEMESSAGE', $self->OnCompleteMessage))
125
			);
126
127
			$onCompleteFieldSet->addExtraClass('field');
128
129
			$editor->setRows(3);
130
			$label->addExtraClass('left');
131
132
			// Define config for email recipients
133
			$emailRecipientsConfig = GridFieldConfig_RecordEditor::create(10);
134
			$emailRecipientsConfig->getComponentByType('GridFieldAddNewButton')
135
				->setButtonName(
136
					_t('UserDefinedForm.ADDEMAILRECIPIENT', 'Add Email Recipient')
137
				);
138
139
			// who do we email on submission
140
			$emailRecipients = new GridField(
141
				'EmailRecipients',
142
				_t('UserDefinedForm.EMAILRECIPIENTS', 'Email Recipients'),
143
				$self->EmailRecipients(),
144
				$emailRecipientsConfig
145
			);
146
			$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...
147
				->getConfig()
148
				->getComponentByType('GridFieldDetailForm')
149
				->setItemRequestClass('UserFormRecipientItemRequest');
150
151
			$fields->addFieldsToTab('Root.FormOptions', $onCompleteFieldSet);
152
			$fields->addFieldToTab('Root.Recipients', $emailRecipients);
153
			$fields->addFieldsToTab('Root.FormOptions', $self->getFormOptions());
154
155
156
			// view the submissions
157
			$submissions = new GridField(
158
				'Submissions',
159
				_t('UserDefinedForm.SUBMISSIONS', 'Submissions'),
160
				 $self->Submissions()->sort('Created', 'DESC')
161
			);
162
163
			// make sure a numeric not a empty string is checked against this int column for SQL server
164
			$parentID = (!empty($self->ID)) ? (int) $self->ID : 0;
165
166
			// get a list of all field names and values used for print and export CSV views of the GridField below.
167
			$columnSQL = <<<SQL
168
SELECT "Name", "Title"
169
FROM "SubmittedFormField"
170
LEFT JOIN "SubmittedForm" ON "SubmittedForm"."ID" = "SubmittedFormField"."ParentID"
171
WHERE "SubmittedForm"."ParentID" = '$parentID'
172
ORDER BY "Title" ASC
173
SQL;
174
			// Sanitise periods in title
175
			$columns = array();
176
			foreach(DB::query($columnSQL)->map() as $name => $title) {
177
				$columns[$name] = trim(strtr($title, '.', ' '));
178
			}
179
180
			$config = new GridFieldConfig();
181
			$config->addComponent(new GridFieldToolbarHeader());
182
			$config->addComponent($sort = new GridFieldSortableHeader());
183
			$config->addComponent($filter = new UserFormsGridFieldFilterHeader());
184
			$config->addComponent(new GridFieldDataColumns());
185
			$config->addComponent(new GridFieldEditButton());
186
			$config->addComponent(new GridFieldDeleteAction());
187
			$config->addComponent(new GridFieldPageCount('toolbar-header-right'));
188
			$config->addComponent($pagination = new GridFieldPaginator(25));
189
			$config->addComponent(new GridFieldDetailForm());
190
			$config->addComponent(new GridFieldButtonRow('after'));
191
			$config->addComponent($export = new GridFieldExportButton('buttons-after-left'));
192
			$config->addComponent($print = new GridFieldPrintButton('buttons-after-left'));
193
194
			/**
195
			 * Support for {@link https://github.com/colymba/GridFieldBulkEditingTools}
196
			 */
197
			if(class_exists('GridFieldBulkManager')) {
198
				$config->addComponent(new GridFieldBulkManager());
199
			}
200
201
			$sort->setThrowExceptionOnBadDataType(false);
202
			$filter->setThrowExceptionOnBadDataType(false);
203
			$pagination->setThrowExceptionOnBadDataType(false);
204
205
			// attach every column to the print view form
206
			$columns['Created'] = 'Created';
207
			$filter->setColumns($columns);
208
209
			// print configuration
210
211
			$print->setPrintHasHeader(true);
212
			$print->setPrintColumns($columns);
213
214
			// export configuration
215
			$export->setCsvHasHeader(true);
216
			$export->setExportColumns($columns);
217
218
			$submissions->setConfig($config);
219
			$fields->addFieldToTab('Root.Submissions', $submissions);
220
			$fields->addFieldToTab('Root.FormOptions', new CheckboxField('DisableSaveSubmissions', _t('UserDefinedForm.SAVESUBMISSIONS', 'Disable Saving Submissions to Server')));
221
222
		});
223
224
		$fields = parent::getCMSFields();
225
226
		return $fields;
227
	}
228
229
	/**
230
	 * Allow overriding the EmailRecipients on a {@link DataExtension}
231
	 * so you can customise who receives an email.
232
	 * Converts the RelationList to an ArrayList so that manipulation
233
	 * of the original source data isn't possible.
234
	 *
235
	 * @return ArrayList
236
	 */
237
	public function FilteredEmailRecipients($data = null, $form = null) {
238
		$recipients = new ArrayList($this->EmailRecipients()->toArray());
239
240
		// Filter by rules
241
		$recipients = $recipients->filterByCallback(function($recipient) use ($data, $form) {
242
			return $recipient->canSend($data, $form);
243
		});
244
245
		$this->extend('updateFilteredEmailRecipients', $recipients, $data, $form);
246
247
		return $recipients;
248
	}
249
250
	/**
251
	 * Custom options for the form. You can extend the built in options by
252
	 * using {@link updateFormOptions()}
253
	 *
254
	 * @return FieldList
255
	 */
256
	public function getFormOptions() {
257
		$submit = ($this->SubmitButtonText) ? $this->SubmitButtonText : _t('UserDefinedForm.SUBMITBUTTON', 'Submit');
258
		$clear = ($this->ClearButtonText) ? $this->ClearButtonText : _t('UserDefinedForm.CLEARBUTTON', 'Clear');
259
260
		$options = new FieldList(
261
			new TextField("SubmitButtonText", _t('UserDefinedForm.TEXTONSUBMIT', 'Text on submit button:'), $submit),
262
			new TextField("ClearButtonText", _t('UserDefinedForm.TEXTONCLEAR', 'Text on clear button:'), $clear),
263
			new CheckboxField("ShowClearButton", _t('UserDefinedForm.SHOWCLEARFORM', 'Show Clear Form Button'), $this->ShowClearButton),
264
			new CheckboxField("EnableLiveValidation", _t('UserDefinedForm.ENABLELIVEVALIDATION', 'Enable live validation')),
265
			new CheckboxField("HideFieldLabels", _t('UserDefinedForm.HIDEFIELDLABELS', 'Hide field labels')),
266
			new CheckboxField("DisplayErrorMessagesAtTop", _t('UserDefinedForm.DISPLAYERRORMESSAGESATTOP', 'Display error messages above the form?')),
267
			new CheckboxField('DisableCsrfSecurityToken', _t('UserDefinedForm.DISABLECSRFSECURITYTOKEN', 'Disable CSRF Token')),
268
			new CheckboxField('DisableAuthenicatedFinishAction', _t('UserDefinedForm.DISABLEAUTHENICATEDFINISHACTION', 'Disable Authentication on finish action'))
269
		);
270
271
		$this->extend('updateFormOptions', $options);
272
273
		return $options;
274
	}
275
276
	/**
277
	 * Get the HTML id of the error container displayed above the form.
278
	 *
279
	 * @return string
280
	 */
281
	public function getErrorContainerID() {
282
		return $this->config()->error_container_id;
283
	}
284
285
	public function requireDefaultRecords() {
286
		parent::requireDefaultRecords();
287
288
		if(!$this->config()->upgrade_on_build) {
289
			return;
290
		}
291
292
		// Perform migrations
293
		Injector::inst()
294
			->create('UserFormsUpgradeService')
295
			->setQuiet(true)
296
			->run();
297
298
		DB::alteration_message('Migrated userforms', 'changed');
299
	}
300
301
302
	/**
303
	 * Validate formfields
304
	 */
305
	public function getCMSValidator() {
306
		return new UserFormValidator();
307
	}
308
}
309
310
/**
311
 * Controller for the {@link UserDefinedForm} page type.
312
 *
313
 * @package userforms
314
 */
315
316
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...
317
318
	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...
319
320
	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...
321
		'index',
322
		'ping',
323
		'Form',
324
		'finished'
325
	);
326
327
	public function init() {
328
		parent::init();
329
330
		// load the jquery
331
		$lang = i18n::get_lang_from_locale(i18n::get_locale());
332
		Requirements::css(USERFORMS_DIR . '/css/UserForm.css');
333
		Requirements::javascript(FRAMEWORK_DIR .'/thirdparty/jquery/jquery.js');
334
		Requirements::javascript(USERFORMS_DIR . '/thirdparty/jquery-validate/jquery.validate.min.js');
335
		Requirements::add_i18n_javascript(USERFORMS_DIR . '/javascript/lang');
336
		Requirements::javascript(USERFORMS_DIR . '/javascript/UserForm.js');
337
338
		Requirements::javascript(
339
			USERFORMS_DIR . "/thirdparty/jquery-validate/localization/messages_{$lang}.min.js"
340
		);
341
		Requirements::javascript(
342
			USERFORMS_DIR . "/thirdparty/jquery-validate/localization/methods_{$lang}.min.js"
343
		);
344
		if($this->HideFieldLabels) {
345
			Requirements::javascript(USERFORMS_DIR . '/thirdparty/Placeholders.js/Placeholders.min.js');
346
		}
347
348
		// Bind a confirmation message when navigating away from a partially completed form.
349
		$page = $this->data();
350
		if($page::config()->enable_are_you_sure) {
351
			Requirements::javascript(USERFORMS_DIR . '/thirdparty/jquery.are-you-sure/jquery.are-you-sure.js');
352
		}
353
	}
354
355
	/**
356
	 * Using $UserDefinedForm in the Content area of the page shows
357
	 * where the form should be rendered into. If it does not exist
358
	 * then default back to $Form.
359
	 *
360
	 * @return array
361
	 */
362
	public function index() {
363
		if($this->Content && $form = $this->Form()) {
364
			$hasLocation = stristr($this->Content, '$UserDefinedForm');
365
			if($hasLocation) {
366
				$content = preg_replace('/(<p[^>]*>)?\\$UserDefinedForm(<\\/p>)?/i', $form->forTemplate(), $this->Content);
367
				return array(
368
					'Content' => DBField::create_field('HTMLText', $content),
369
					'Form' => ""
370
				);
371
			}
372
		}
373
374
		return array(
375
			'Content' => DBField::create_field('HTMLText', $this->Content),
376
			'Form' => $this->Form()
377
		);
378
	}
379
380
	/**
381
	 * Keep the session alive for the user.
382
	 *
383
	 * @return int
384
	 */
385
	public function ping() {
386
		return 1;
387
	}
388
389
	/**
390
	 * Get the form for the page. Form can be modified by calling {@link updateForm()}
391
	 * on a UserDefinedForm extension.
392
	 *
393
	 * @return Forms
394
	 */
395
	public function Form() {
396
		$form = UserForm::create($this);
397
		$this->generateConditionalJavascript();
398
		return $form;
399
	}
400
401
	/**
402
	 * Generate the javascript for the conditional field show / hiding logic.
403
	 *
404
	 * @return void
405
	 */
406
	public function generateConditionalJavascript() {
407
		$default = "";
408
		$rules = "";
409
410
		$watch = array();
411
		$watchLoad = array();
412
413
		if($this->Fields()) {
414
			foreach($this->Fields() as $field) {
415
				$holderSelector = $field->getSelectorHolder();
416
417
				// Is this Field Show by Default
418
				if(!$field->ShowOnLoad) {
419
					$default .= "{$holderSelector}.hide().trigger('userform.field.hide');\n";
420
				}
421
422
				// Check for field dependencies / default
423
				foreach($field->DisplayRules() as $rule) {
424
425
					// Get the field which is effected
426
					$formFieldWatch = EditableFormField::get()->byId($rule->ConditionFieldID);
427
428
					// Skip deleted fields
429
					if(!$formFieldWatch) {
430
						continue;
431
					}
432
433
					$fieldToWatch = $formFieldWatch->getSelectorField($rule);
434
					$fieldToWatchOnLoad = $formFieldWatch->getSelectorField($rule, true);
435
436
					// show or hide?
437
					$view = ($rule->Display == 'Hide') ? 'hide' : 'show';
438
					$opposite = ($view == "show") ? "hide" : "show";
439
440
					// what action do we need to keep track of. Something nicer here maybe?
441
					// @todo encapulsation
442
					$action = "change";
443
444
					if($formFieldWatch instanceof EditableTextField) {
445
						$action = "keyup";
446
					}
447
448
					// is this field a special option field
449
					$checkboxField = false;
450
					$radioField = false;
451
452
					if(in_array($formFieldWatch->ClassName, array('EditableCheckboxGroupField', 'EditableCheckbox'))) {
453
						$action = "click";
454
						$checkboxField = true;
455
					} else if ($formFieldWatch->ClassName == "EditableRadioField") {
456
						$radioField = true;
457
					}
458
459
					// and what should we evaluate
460
					switch($rule->ConditionOption) {
461
						case 'IsNotBlank':
462
							$expression = ($checkboxField || $radioField) ? '$(this).is(":checked")' :'$(this).val() != ""';
463
464
							break;
465
						case 'IsBlank':
466
							$expression = ($checkboxField || $radioField) ? '!($(this).is(":checked"))' : '$(this).val() == ""';
467
468
							break;
469 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...
470
							if ($checkboxField) {
471
								$expression = '$(this).prop("checked")';
472
							} else if ($radioField) {
473
								// We cannot simply get the value of the radio group, we need to find the checked option first.
474
								$expression = '$(this).parents(".field, .control-group").find("input:checked").val()=="'. $rule->FieldValue .'"';
475
							} else {
476
								$expression = '$(this).val() == "'. $rule->FieldValue .'"';
477
							}
478
479
							break;
480
						case 'ValueLessThan':
481
							$expression = '$(this).val() < parseFloat("'. $rule->FieldValue .'")';
482
483
							break;
484
						case 'ValueLessThanEqual':
485
							$expression = '$(this).val() <= parseFloat("'. $rule->FieldValue .'")';
486
487
							break;
488
						case 'ValueGreaterThan':
489
							$expression = '$(this).val() > parseFloat("'. $rule->FieldValue .'")';
490
491
							break;
492
						case 'ValueGreaterThanEqual':
493
							$expression = '$(this).val() >= parseFloat("'. $rule->FieldValue .'")';
494
495
							break;
496 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...
497
							if ($checkboxField) {
498
								$expression = '!$(this).prop("checked")';
499
							} else if ($radioField) {
500
								// We cannot simply get the value of the radio group, we need to find the checked option first.
501
								$expression = '$(this).parents(".field, .control-group").find("input:checked").val()!="'. $rule->FieldValue .'"';
502
							} else {
503
								$expression = '$(this).val() != "'. $rule->FieldValue .'"';
504
							}
505
506
							break;
507
					}
508
509
					if(!isset($watch[$fieldToWatch])) {
510
						$watch[$fieldToWatch] = array();
511
					}
512
513
					$watch[$fieldToWatch][] = array(
514
						'expression' => $expression,
515
						'holder_selector' => $holderSelector,
516
						'view' => $view,
517
						'opposite' => $opposite,
518
						'action' => $action
519
					);
520
521
					$watchLoad[$fieldToWatchOnLoad] = true;
522
				}
523
			}
524
		}
525
526
		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...
527
			foreach($watch as $key => $values) {
528
				$logic = array();
529
				$actions = array();
530
531
				foreach($values as $rule) {
532
					// Assign action
533
					$actions[$rule['action']] = $rule['action'];
534
535
					// Assign behaviour
536
					$expression = $rule['expression'];
537
					$holder = $rule['holder_selector'];
538
					$view = $rule['view']; // hide or show
539
					$opposite = $rule['opposite'];
540
					// Generated javascript for triggering visibility
541
					$logic[] = <<<"EOS"
542
if({$expression}) {
543
	{$holder}
544
		.{$view}()
545
		.trigger('userform.field.{$view}');
546
} else {
547
	{$holder}
548
		.{$opposite}()
549
		.trigger('userform.field.{$opposite}');
550
}
551
EOS;
552
				}
553
554
				$logic = implode("\n", $logic);
555
				$rules .= $key.".each(function() {\n
556
	$(this).data('userformConditions', function() {\n
557
		$logic\n
558
	}); \n
559
});\n";
560
				foreach($actions as $action) {
561
					$rules .= $key.".$action(function() {
562
	$(this).data('userformConditions').call(this);\n
563
});\n";
564
				}
565
			}
566
		}
567
568
		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...
569
			foreach($watchLoad as $key => $value) {
570
				$rules .= $key.".each(function() {
571
	$(this).data('userformConditions').call(this);\n
572
});\n";
573
			}
574
		}
575
576
		// Only add customScript if $default or $rules is defined
577
		if($default  || $rules) {
578
			Requirements::customScript(<<<JS
579
				(function($) {
580
					$(document).ready(function() {
581
						$default
582
583
						$rules
584
					})
585
				})(jQuery);
586
JS
587
, 'UserFormsConditional');
588
		}
589
	}
590
591
	/**
592
	 * Process the form that is submitted through the site
593
	 *
594
	 * {@see UserForm::validate()} for validation step prior to processing
595
	 *
596
	 * @param array $data
597
	 * @param Form $form
598
	 *
599
	 * @return Redirection
600
	 */
601
	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...
602
		$submittedForm = Object::create('SubmittedForm');
603
		$submittedForm->SubmittedByID = ($id = Member::currentUserID()) ? $id : 0;
604
		$submittedForm->ParentID = $this->ID;
605
606
		// if saving is not disabled save now to generate the ID
607
		if(!$this->DisableSaveSubmissions) {
608
			$submittedForm->write();
609
		}
610
611
		$attachments = array();
612
		$submittedFields = new ArrayList();
613
614
		foreach($this->Fields() as $field) {
615
			if(!$field->showInReports()) {
616
				continue;
617
			}
618
619
			$submittedField = $field->getSubmittedFormField();
620
			$submittedField->ParentID = $submittedForm->ID;
621
			$submittedField->Name = $field->Name;
622
			$submittedField->Title = $field->getField('Title');
623
624
			// save the value from the data
625
			if($field->hasMethod('getValueFromData')) {
626
				$submittedField->Value = $field->getValueFromData($data);
627
			} else {
628
				if(isset($data[$field->Name])) {
629
					$submittedField->Value = $data[$field->Name];
630
				}
631
			}
632
633
			if(!empty($data[$field->Name])) {
634
				if(in_array("EditableFileField", $field->getClassAncestry())) {
635
					if(isset($_FILES[$field->Name])) {
636
						$foldername = $field->getFormField()->getFolderName();
637
638
						// create the file from post data
639
						$upload = new Upload();
640
						$file = new File();
641
						$file->ShowInSearch = 0;
642
						try {
643
							$upload->loadIntoFile($_FILES[$field->Name], $file, $foldername);
644
						} catch( ValidationException $e ) {
645
							$validationResult = $e->getResult();
646
							$form->addErrorMessage($field->Name, $validationResult->message(), 'bad');
647
							Controller::curr()->redirectBack();
648
							return;
649
						}
650
651
						// write file to form field
652
						$submittedField->UploadedFileID = $file->ID;
653
654
						// attach a file only if lower than 1MB
655
						if($file->getAbsoluteSize() < 1024*1024*1) {
656
							$attachments[] = $file;
657
						}
658
					}
659
				}
660
			}
661
662
			$submittedField->extend('onPopulationFromField', $field);
663
664
			if(!$this->DisableSaveSubmissions) {
665
				$submittedField->write();
666
			}
667
668
			$submittedFields->push($submittedField);
669
		}
670
671
		$emailData = array(
672
			"Sender" => Member::currentUser(),
673
			"Fields" => $submittedFields
674
		);
675
676
		$this->extend('updateEmailData', $emailData, $attachments);
677
678
		// email users on submit.
679
		if($recipients = $this->FilteredEmailRecipients($data, $form)) {
680
681
			foreach($recipients as $recipient) {
682
				$email = new UserFormRecipientEmail($submittedFields);
683
				$mergeFields = $this->getMergeFieldsMap($emailData['Fields']);
684
	
685
				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...
686
					foreach($attachments as $file) {
687
						if($file->ID != 0) {
688
							$email->attachFile(
689
								$file->Filename,
690
								$file->Filename,
691
								HTTP::get_mime_type($file->Filename)
692
							);
693
						}
694
					}
695
				}
696
				
697
				$parsedBody = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields);
698
699
				if (!$recipient->SendPlain && $recipient->emailTemplateExists()) {
700
					$email->setTemplate($recipient->EmailTemplate);
701
				}
702
703
				$email->populateTemplate($recipient);
704
				$email->populateTemplate($emailData);
705
				$email->setFrom($recipient->EmailFrom);
706
				$email->setBody($parsedBody);
707
				$email->setTo($recipient->EmailAddress);
708
				$email->setSubject($recipient->EmailSubject);
709
710
				if($recipient->EmailReplyTo) {
711
					$email->setReplyTo($recipient->EmailReplyTo);
712
				}
713
714
				// check to see if they are a dynamic reply to. eg based on a email field a user selected
715 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...
716
					$submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name);
717
718
					if($submittedFormField && is_string($submittedFormField->Value)) {
719
						$email->setReplyTo($submittedFormField->Value);
720
					}
721
				}
722
				// check to see if they are a dynamic reciever eg based on a dropdown field a user selected
723 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...
724
					$submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name);
725
726
					if($submittedFormField && is_string($submittedFormField->Value)) {
727
						$email->setTo($submittedFormField->Value);
728
					}
729
				}
730
731
				// check to see if there is a dynamic subject
732 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...
733
					$submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name);
734
735
					if($submittedFormField && trim($submittedFormField->Value)) {
736
						$email->setSubject($submittedFormField->Value);
737
					}
738
				}
739
740
				$this->extend('updateEmail', $email, $recipient, $emailData);
741
742
				if($recipient->SendPlain) {
743
					$body = strip_tags($recipient->getEmailBodyContent()) . "\n";
744
					if(isset($emailData['Fields']) && !$recipient->HideFormData) {
745
						foreach($emailData['Fields'] as $Field) {
746
							$body .= $Field->Title .': '. $Field->Value ." \n";
747
						}
748
					}
749
750
					$email->setBody($body);
751
					$email->sendPlain();
752
				}
753
				else {
754
					$email->send();
755
				}
756
			}
757
		}
758
759
		$submittedForm->extend('updateAfterProcess');
760
761
		Session::clear("FormInfo.{$form->FormName()}.errors");
762
		Session::clear("FormInfo.{$form->FormName()}.data");
763
764
		$referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : "";
765
766
		// set a session variable from the security ID to stop people accessing
767
		// the finished method directly.
768
		if(!$this->DisableAuthenicatedFinishAction) {
769
			if (isset($data['SecurityID'])) {
770
				Session::set('FormProcessed',$data['SecurityID']);
771
			} else {
772
				// if the form has had tokens disabled we still need to set FormProcessed
773
				// to allow us to get through the finshed method
774
				if (!$this->Form()->getSecurityToken()->isEnabled()) {
775
					$randNum = rand(1, 1000);
776
					$randHash = md5($randNum);
777
					Session::set('FormProcessed',$randHash);
778
					Session::set('FormProcessedNum',$randNum);
779
				}
780
			}
781
		}
782
783
		if(!$this->DisableSaveSubmissions) {
784
			Session::set('userformssubmission'. $this->ID, $submittedForm->ID);
785
		}
786
787
		return $this->redirect($this->Link('finished') . $referrer . $this->config()->finished_anchor);
788
	}
789
790
	/**
791
	 * Allows the use of field values in email body.
792
	 *
793
	 * @param ArrayList fields
794
	 * @return ArrayData
795
	 */
796
	private function getMergeFieldsMap($fields = array()) {
797
		$data = new ArrayData(array());
798
799
		foreach ($fields as $field) {
800
			$data->setField($field->Name, DBField::create_field('Text', $field->Value));
801
		}
802
803
		return $data;
804
	}
805
806
	/**
807
	 * This action handles rendering the "finished" message, which is
808
	 * customizable by editing the ReceivedFormSubmission template.
809
	 *
810
	 * @return ViewableData
811
	 */
812
	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...
813
		$submission = Session::get('userformssubmission'. $this->ID);
814
815
		if($submission) {
816
			$submission = SubmittedForm::get()->byId($submission);
817
		}
818
819
		$referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null;
820
821
		if(!$this->DisableAuthenicatedFinishAction) {
822
			$formProcessed = Session::get('FormProcessed');
823
824
			if (!isset($formProcessed)) {
825
				return $this->redirect($this->Link() . $referrer);
826
			} else {
827
				$securityID = Session::get('SecurityID');
828
				// make sure the session matches the SecurityID and is not left over from another form
829
				if ($formProcessed != $securityID) {
830
					// they may have disabled tokens on the form
831
					$securityID = md5(Session::get('FormProcessedNum'));
832
					if ($formProcessed != $securityID) {
833
						return $this->redirect($this->Link() . $referrer);
834
					}
835
				}
836
			}
837
838
			Session::clear('FormProcessed');
839
		}
840
841
		return $this->customise(array(
842
			'Content' => $this->customise(array(
843
				'Submission' => $submission,
844
				'Link' => $referrer
845
			))->renderWith('ReceivedFormSubmission'),
846
			'Form' => '',
847
		));
848
	}
849
}
850