Passed
Push — master ( ebdfbb...20570e )
by
unknown
02:32
created

code/Model/Recipient/EmailRecipient.php (2 issues)

1
<?php
2
3
namespace SilverStripe\UserForms\Model\Recipient;
4
5
use SilverStripe\Admin\LeftAndMain;
6
use SilverStripe\Assets\FileFinder;
7
use SilverStripe\CMS\Controllers\CMSMain;
8
use SilverStripe\CMS\Controllers\CMSPageEditController;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Email\Email;
12
use SilverStripe\Core\Manifest\ModuleResource;
13
use SilverStripe\Core\Manifest\ModuleResourceLoader;
14
use SilverStripe\Forms\CheckboxField;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\FieldGroup;
17
use SilverStripe\Forms\FieldList;
18
use SilverStripe\Forms\Form;
19
use SilverStripe\Forms\GridField\GridField;
20
use SilverStripe\Forms\GridField\GridFieldButtonRow;
21
use SilverStripe\Forms\GridField\GridFieldConfig;
22
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
23
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
24
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
25
use SilverStripe\Forms\LiteralField;
26
use SilverStripe\Forms\TabSet;
27
use SilverStripe\Forms\TextareaField;
28
use SilverStripe\Forms\TextField;
29
use SilverStripe\ORM\ArrayList;
30
use SilverStripe\ORM\DataObject;
31
use SilverStripe\ORM\DB;
32
use SilverStripe\ORM\FieldType\DBField;
33
use SilverStripe\UserForms\Model\EditableFormField;
34
use SilverStripe\UserForms\Model\EditableFormField\EditableEmailField;
35
use SilverStripe\UserForms\Model\EditableFormField\EditableMultipleOptionField;
36
use SilverStripe\UserForms\Model\EditableFormField\EditableTextField;
37
use SilverStripe\UserForms\Model\UserDefinedForm;
38
use SilverStripe\View\Requirements;
39
use Symbiote\GridFieldExtensions\GridFieldAddNewInlineButton;
40
use Symbiote\GridFieldExtensions\GridFieldEditableColumns;
41
42
/**
43
 * A Form can have multiply members / emails to email the submission
44
 * to and custom subjects
45
 *
46
 * @package userforms
47
 */
48
class EmailRecipient extends DataObject
49
{
50
    private static $db = [
51
        'EmailAddress' => 'Varchar(200)',
52
        'EmailSubject' => 'Varchar(200)',
53
        'EmailFrom' => 'Varchar(200)',
54
        'EmailReplyTo' => 'Varchar(200)',
55
        'EmailBody' => 'Text',
56
        'EmailBodyHtml' => 'HTMLText',
57
        'EmailTemplate' => 'Varchar',
58
        'SendPlain' => 'Boolean',
59
        'HideFormData' => 'Boolean',
60
        'CustomRulesCondition' => 'Enum("And,Or")'
61
    ];
62
63
    private static $has_one = [
64
        'Form' => DataObject::class,
65
        'SendEmailFromField' => EditableFormField::class,
66
        'SendEmailToField' => EditableFormField::class,
67
        'SendEmailSubjectField' => EditableFormField::class
68
    ];
69
70
    private static $has_many = [
71
        'CustomRules' => EmailRecipientCondition::class,
72
    ];
73
74
    private static $owns = [
75
        'CustomRules',
76
    ];
77
78
    private static $cascade_deetes = [
79
        'CustomRules',
80
    ];
81
82
    private static $summary_fields = [
83
        'EmailAddress',
84
        'EmailSubject',
85
        'EmailFrom'
86
    ];
87
88
    private static $table_name = 'UserDefinedForm_EmailRecipient';
89
90
    /**
91
     * Setting this to true will allow you to select "risky" fields as
92
     * email recipient, such as free-text entry fields.
93
     *
94
     * It's advisable to leave this off.
95
     *
96
     * @config
97
     * @var bool
98
     */
99
    private static $allow_unbound_recipient_fields = false;
100
101
    public function requireDefaultRecords()
102
    {
103
        parent::requireDefaultRecords();
104
105
        // make sure to migrate the class across (prior to v5.x)
106
        DB::query("UPDATE UserDefinedForm_EmailRecipient SET FormClass = 'Page' WHERE FormClass IS NULL");
107
    }
108
109
    public function summaryFields()
110
    {
111
        $fields = parent::summaryFields();
112
        if (isset($fields['EmailAddress'])) {
113
            /** @skipUpgrade */
114
            $fields['EmailAddress'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILADDRESS', 'Email');
115
        }
116
        if (isset($fields['EmailSubject'])) {
117
            $fields['EmailSubject'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Subject');
118
        }
119
        if (isset($fields['EmailFrom'])) {
120
            $fields['EmailFrom'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILFROM', 'From');
121
        }
122
        return $fields;
123
    }
124
125
    /**
126
     * Get instance of UserDefinedForm when editing in getCMSFields
127
     *
128
     * @return UserDefinedForm
129
     */
130
    protected function getFormParent()
131
    {
132
        // LeftAndMain::sessionNamespace is protected. @todo replace this with a non-deprecated equivalent.
133
        $sessionNamespace = $this->config()->get('session_namespace') ?: CMSMain::class;
134
135
        $formID = $this->FormID
136
            ? $this->FormID
137
            : Controller::curr()->getRequest()->getSession()->get($sessionNamespace . '.currentPage');
138
        return UserDefinedForm::get()->byID($formID);
139
    }
140
141
    public function getTitle()
142
    {
143
        if ($this->EmailAddress) {
144
            return $this->EmailAddress;
145
        }
146
        if ($this->EmailSubject) {
147
            return $this->EmailSubject;
148
        }
149
        return parent::getTitle();
150
    }
151
152
    /**
153
     * Generate a gridfield config for editing filter rules
154
     *
155
     * @return GridFieldConfig
156
     */
157
    protected function getRulesConfig()
158
    {
159
        $formFields = $this->getFormParent()->Fields();
160
161
        $config = GridFieldConfig::create()
162
            ->addComponents(
163
                new GridFieldButtonRow('before'),
164
                new GridFieldToolbarHeader(),
165
                new GridFieldAddNewInlineButton(),
166
                new GridFieldDeleteAction(),
167
                $columns = new GridFieldEditableColumns()
168
            );
169
170
        $columns->setDisplayFields(array(
171
            'ConditionFieldID' => function ($record, $column, $grid) use ($formFields) {
172
                return DropdownField::create($column, false, $formFields->map('ID', 'Title'));
173
            },
174
            'ConditionOption' => function ($record, $column, $grid) {
175
                $options = EmailRecipientCondition::config()->condition_options;
176
                return DropdownField::create($column, false, $options);
177
            },
178
            'ConditionValue' => function ($record, $column, $grid) {
179
                return TextField::create($column);
180
            }
181
        ));
182
183
        return $config;
184
    }
185
186
    /**
187
     * @return FieldList
188
     */
189
    public function getCMSFields()
190
    {
191
        Requirements::javascript('silverstripe/userforms:client/dist/js/userforms-cms.js');
192
193
        // Determine optional field values
194
        $form = $this->getFormParent();
195
196
        // predefined choices are also candidates
197
        $multiOptionFields = EditableMultipleOptionField::get()->filter('ParentID', $form->ID);
198
199
        // if they have email fields then we could send from it
200
        $validEmailFromFields = EditableEmailField::get()->filter('ParentID', $form->ID);
201
202
        // For the subject, only one-line entry boxes make sense
203
        $validSubjectFields = ArrayList::create(
204
            EditableTextField::get()
205
                ->filter('ParentID', $form->ID)
206
                ->exclude('Rows:GreaterThan', 1)
207
                ->toArray()
208
        );
209
        $validSubjectFields->merge($multiOptionFields);
210
211
212
        // Check valid email-recipient fields
213
        if ($this->config()->get('allow_unbound_recipient_fields')) {
214
            // To address can only be email fields or multi option fields
215
            $validEmailToFields = ArrayList::create($validEmailFromFields->toArray());
216
            $validEmailToFields->merge($multiOptionFields);
217
        } else {
218
            // To address cannot be unbound, so restrict to pre-defined lists
219
            $validEmailToFields = $multiOptionFields;
220
        }
221
222
        // Build fieldlist
223
        $fields = FieldList::create(Tabset::create('Root')->addExtraClass('EmailRecipientForm'));
224
225
        // Configuration fields
226
        $fields->addFieldsToTab('Root.EmailDetails', [
227
            // Subject
228
            FieldGroup::create(
229
                TextField::create(
230
                    'EmailSubject',
231
                    _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPESUBJECT', 'Type subject')
232
                )
233
                    ->setAttribute('style', 'min-width: 400px;'),
234
                DropdownField::create(
235
                    'SendEmailSubjectFieldID',
236
                    _t(
237
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.SELECTAFIELDTOSETSUBJECT',
238
                        '.. or select a field to use as the subject'
239
                    ),
240
                    $validSubjectFields->map('ID', 'Title')
241
                )->setEmptyString('')
242
            )
243
                ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Email subject')),
244
245
            // To
246
            FieldGroup::create(
247
                TextField::create(
248
                    'EmailAddress',
249
                    _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPETO', 'Type to address')
250
                )
251
                    ->setAttribute('style', 'min-width: 400px;'),
252
                DropdownField::create(
253
                    'SendEmailToFieldID',
254
                    _t(
255
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASTO',
256
                        '.. or select a field to use as the to address'
257
                    ),
258
                    $validEmailToFields->map('ID', 'Title')
259
                )->setEmptyString(' ')
260
            )
261
                ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO', 'Send email to'))
262
                ->setDescription(_t(
263
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO_DESCRIPTION',
264
                    'You may enter multiple email addresses as a comma separated list.'
265
                )),
266
267
268
            // From
269
            TextField::create(
270
                'EmailFrom',
271
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.FROMADDRESS', 'Send email from')
272
            )
273
                ->setDescription(_t(
274
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.EmailFromContent',
275
                    "The from address allows you to set who the email comes from. On most servers this ".
276
                    "will need to be set to an email address on the same domain name as your site. ".
277
                    "For example on yoursite.com the from address may need to be [email protected]. ".
278
                    "You can however, set any email address you wish as the reply to address."
279
                )),
280
281
282
            // Reply-To
283
            FieldGroup::create(
284
                TextField::create('EmailReplyTo', _t(
285
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPEREPLY',
286
                    'Type reply address'
287
                ))
288
                    ->setAttribute('style', 'min-width: 400px;'),
289
                DropdownField::create(
290
                    'SendEmailFromFieldID',
291
                    _t(
292
                        'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASFROM',
293
                        '.. or select a field to use as reply to address'
294
                    ),
295
                    $validEmailFromFields->map('ID', 'Title')
296
                )->setEmptyString(' ')
297
            )
298
                ->setTitle(_t(
299
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS',
300
                    'Email for reply to'
301
                ))
302
                ->setDescription(_t(
303
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS_DESCRIPTION',
304
                    'The email address which the recipient is able to \'reply\' to.'
305
                ))
306
        ]);
307
308
        $fields->fieldByName('Root.EmailDetails')->setTitle(_t(__CLASS__.'.EMAILDETAILSTAB', 'Email Details'));
309
310
        // Only show the preview link if the recipient has been saved.
311
        if (!empty($this->EmailTemplate)) {
312
            $pageEditController = singleton(CMSPageEditController::class);
313
            $pageEditController
314
                ->getRequest()
315
                ->setSession(Controller::curr()->getRequest()->getSession());
316
317
            $preview = sprintf(
318
                '<p><a href="%s" target="_blank" class="btn btn-outline-secondary">%s</a></p><em>%s</em>',
319
                Controller::join_links(
320
                    $pageEditController->getEditForm()->FormAction(),
321
                    "field/EmailRecipients/item/{$this->ID}/preview"
322
                ),
323
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL', 'Preview email'),
324
                _t(
325
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_DESCRIPTION',
326
                    'Note: Unsaved changes will not appear in the preview.'
327
                )
328
            );
329
        } else {
330
            $preview = sprintf(
331
                '<em>%s</em>',
332
                _t(
333
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_UNAVAILABLE',
334
                    'You can preview this email once you have saved the Recipient.'
335
                )
336
            );
337
        }
338
339
        // Email templates
340
        $fields->addFieldsToTab('Root.EmailContent', [
341
            CheckboxField::create(
342
                'HideFormData',
343
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.HIDEFORMDATA', 'Hide form data from email?')
344
            ),
345
            CheckboxField::create(
346
                'SendPlain',
347
                _t(
348
                    'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDPLAIN',
349
                    'Send email as plain text? (HTML will be stripped)'
350
                )
351
            ),
352
            HTMLEditorField::create(
353
                'EmailBodyHtml',
354
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODYHTML', 'Body')
355
            )
356
                ->addExtraClass('toggle-html-only'),
357
            TextareaField::create(
358
                'EmailBody',
359
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODY', 'Body')
360
            )
361
                ->addExtraClass('toggle-plain-only'),
362
            LiteralField::create('EmailPreview', $preview)
363
        ]);
364
365
        $templates = $this->getEmailTemplateDropdownValues();
366
367
        if ($templates) {
368
            $fields->insertBefore(
369
                DropdownField::create(
370
                    'EmailTemplate',
371
                    _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILTEMPLATE', 'Email template'),
372
                    $templates
373
                )->addExtraClass('toggle-html-only'),
374
                'EmailBodyHtml'
375
            );
376
        }
377
378
        $fields->fieldByName('Root.EmailContent')->setTitle(_t(__CLASS__.'.EMAILCONTENTTAB', 'Email Content'));
379
380
        // Custom rules for sending this field
381
        $grid = GridField::create(
382
            'CustomRules',
383
            _t('SilverStripe\\UserForms\\Model\\EditableFormField.CUSTOMRULES', 'Custom Rules'),
384
            $this->CustomRules(),
385
            $this->getRulesConfig()
386
        );
387
        $grid->setDescription(_t(
388
            'SilverStripe\\UserForms\\Model\\UserDefinedForm.RulesDescription',
389
            'Emails will only be sent to the recipient if the custom rules are met. If no rules are defined, this receipient will receive notifications for every submission.'
390
        ));
391
        $fields->addFieldsToTab('Root.CustomRules', [
392
            DropdownField::create(
393
                'CustomRulesCondition',
394
                _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIF', 'Send condition'),
395
                [
396
                    'Or' => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR', 'Any conditions are true'),
397
                    'And' => _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND', 'All conditions are true')
398
                ]
399
            ),
400
            $grid
401
        ]);
402
403
        $fields->fieldByName('Root.CustomRules')->setTitle(_t(__CLASS__.'.CUSTOMRULESTAB', 'Custom Rules'));
404
405
        $this->extend('updateCMSFields', $fields);
406
        return $fields;
407
    }
408
409
    /**
410
     * Return whether a user can create an object of this type
411
     *
412
     * @param Member $member
413
     * @param array $context Virtual parameter to allow context to be passed in to check
414
     * @return bool
415
     */
416
    public function canCreate($member = null, $context = [])
417
    {
418
        // Check parent page
419
        $parent = $this->getCanCreateContext(func_get_args());
420
        if ($parent) {
421
            return $parent->canEdit($member);
422
        }
423
424
        // Fall back to secure admin permissions
425
        return parent::canCreate($member);
426
    }
427
428
    /**
429
     * Helper method to check the parent for this object
430
     *
431
     * @param array $args List of arguments passed to canCreate
432
     * @return SiteTree Parent page instance
433
     */
434
    protected function getCanCreateContext($args)
435
    {
436
        // Inspect second parameter to canCreate for a 'Parent' context
437
        if (isset($args[1][Form::class])) {
438
            return $args[1][Form::class];
439
        }
440
        // Hack in currently edited page if context is missing
441
        if (Controller::has_curr() && Controller::curr() instanceof CMSMain) {
442
            return Controller::curr()->currentPage();
443
        }
444
445
        // No page being edited
446
        return null;
447
    }
448
449
    public function canView($member = null)
450
    {
451
        if ($form = $this->getFormParent()) {
452
            return $form->canView($member);
453
        }
454
        return parent::canView($member);
455
    }
456
457
    public function canEdit($member = null)
458
    {
459
        if ($form = $this->getFormParent()) {
460
            return $form->canEdit($member);
461
        }
462
463
        return parent::canEdit($member);
464
    }
465
466
    /**
467
     * @param Member
468
     *
469
     * @return boolean
470
     */
471
    public function canDelete($member = null)
472
    {
473
        return $this->canEdit($member);
474
    }
475
476
    /**
477
     * Determine if this recipient may receive notifications for this submission
478
     *
479
     * @param array $data
480
     * @param Form $form
481
     * @return bool
482
     */
483
    public function canSend($data, $form)
484
    {
485
        // Skip if no rules configured
486
        $customRules = $this->CustomRules();
487
        if (!$customRules->count()) {
488
            return true;
489
        }
490
491
        // Check all rules
492
        $isAnd = $this->CustomRulesCondition === 'And';
493
        foreach ($customRules as $customRule) {
494
            /** @var EmailRecipientCondition  $customRule */
495
            $matches = $customRule->matches($data);
496
            if ($isAnd && !$matches) {
497
                return false;
498
            }
499
            if (!$isAnd && $matches) {
500
                return true;
501
            }
502
        }
503
504
        // Once all rules are checked
505
        return $isAnd;
506
    }
507
508
    /**
509
     * Make sure the email template saved against the recipient exists on the file system.
510
     *
511
     * @param string
512
     *
513
     * @return boolean
514
     */
515
    public function emailTemplateExists($template = '')
516
    {
517
        $t = ($template ? $template : $this->EmailTemplate);
518
519
        return array_key_exists($t, (array) $this->getEmailTemplateDropdownValues());
520
    }
521
522
    /**
523
     * Get the email body for the current email format
524
     *
525
     * @return string
526
     */
527
    public function getEmailBodyContent()
528
    {
529
        if ($this->SendPlain) {
530
            return DBField::create_field('HTMLText', $this->EmailBody)->Plain();
531
        }
532
        return DBField::create_field('HTMLText', $this->EmailBodyHtml);
533
    }
534
535
    /**
536
     * Gets a list of email templates suitable for populating the email template dropdown.
537
     *
538
     * @return array
539
     */
540
    public function getEmailTemplateDropdownValues()
541
    {
542
        $templates = [];
543
544
        $finder = new FileFinder();
545
        $finder->setOption('name_regex', '/^.*\.ss$/');
546
547
        $parent = $this->getFormParent();
548
549
        if (!$parent) {
550
            return [];
551
        }
552
553
        $emailTemplateDirectory = $parent->config()->get('email_template_directory');
554
        $templateDirectory = ModuleResourceLoader::resourcePath($emailTemplateDirectory);
555
556
        if (!$templateDirectory) {
557
            return [];
558
        }
559
560
        $found = $finder->find(BASE_PATH . DIRECTORY_SEPARATOR . $templateDirectory);
561
562
        foreach ($found as $key => $value) {
563
            $template = pathinfo($value);
564
            $absoluteFilename = $template['dirname'] . DIRECTORY_SEPARATOR . $template['filename'];
565
566
            // Optionally remove vendor/ path prefixes
567
            $resource = ModuleResourceLoader::singleton()->resolveResource($emailTemplateDirectory);
568
            if ($resource instanceof ModuleResource && $resource->getModule()) {
569
                $prefixToStrip = $resource->getModule()->getPath();
570
            } else {
571
                $prefixToStrip = BASE_PATH;
572
            }
573
            $templatePath = substr($absoluteFilename, strlen($prefixToStrip) + 1);
574
575
            // Optionally remove "templates/" prefixes
576
            if (substr($templatePath, 0, 10)) {
577
                $templatePath = substr($templatePath, 10);
578
            }
579
580
            $templates[$templatePath] = $template['filename'];
581
        }
582
583
        return $templates;
584
    }
585
586
    /**
587
     * Validate that valid email addresses are being used
588
     *
589
     * @return ValidationResult
590
     */
591
    public function validate()
592
    {
593
        $result = parent::validate();
594
        $checkEmail = [
595
            'EmailAddress' => 'EMAILADDRESSINVALID',
596
            'EmailFrom' => 'EMAILFROMINVALID',
597
            'EmailReplyTo' => 'EMAILREPLYTOINVALID',
598
        ];
599
        foreach ($checkEmail as $check => $translation) {
600
            if ($this->$check) {
601
                //may be a comma separated list of emails
602
                $addresses = explode(',', $this->$check);
603
                foreach ($addresses as $address) {
604
                    $trimAddress = trim($address);
605
                    if ($trimAddress && !Email::is_valid_address($trimAddress)) {
606
                        $error = _t(
607
                            __CLASS__.".$translation",
608
                            "Invalid email address $trimAddress"
609
                        );
610
                        $result->addError($error . " ($trimAddress)");
611
                    }
612
                }
613
            }
614
        }
615
616
        // if there is no from address and no fallback, you'll have errors if this isn't defined
617
        if (!$this->EmailFrom && empty(Email::getSendAllEmailsFrom()) && empty(Email::config()->get('admin_email'))) {
0 ignored issues
show
Bug Best Practice introduced by
The property EmailFrom does not exist on SilverStripe\UserForms\M...ecipient\EmailRecipient. Since you implemented __get, consider adding a @property annotation.
Loading history...
618
            $result->addError(_t(__CLASS__.".EMAILFROMREQUIRED", '"Email From" address is required'));
619
        }
620
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type SilverStripe\ORM\ValidationResult which is incompatible with the documented return type SilverStripe\UserForms\M...ipient\ValidationResult.
Loading history...
621
    }
622
}
623