'Varchar(200)', 'EmailSubject' => 'Varchar(200)', 'EmailFrom' => 'Varchar(200)', 'EmailReplyTo' => 'Varchar(200)', 'EmailBody' => 'Text', 'EmailBodyHtml' => 'HTMLText', 'EmailTemplate' => 'Varchar', 'SendPlain' => 'Boolean', 'HideFormData' => 'Boolean', 'HideInvisibleFields' => 'Boolean', 'CustomRulesCondition' => 'Enum("And,Or")' ]; private static $has_one = [ 'Form' => DataObject::class, 'SendEmailFromField' => EditableFormField::class, 'SendEmailToField' => EditableFormField::class, 'SendEmailSubjectField' => EditableFormField::class ]; private static $has_many = [ 'CustomRules' => EmailRecipientCondition::class, ]; private static $owns = [ 'CustomRules', ]; private static $cascade_deletes = [ 'CustomRules', ]; private static $summary_fields = [ 'EmailAddress', 'EmailSubject', 'EmailFrom' ]; private static $table_name = 'UserDefinedForm_EmailRecipient'; /** * Disable versioned GridField to ensure that it doesn't interfere with {@link UserFormRecipientItemRequest} * * {@inheritDoc} */ private static $versioned_gridfield_extensions = false; /** * Setting this to true will allow you to select "risky" fields as * email recipient, such as free-text entry fields. * * It's advisable to leave this off. * * @config * @var bool */ private static $allow_unbound_recipient_fields = false; public function requireDefaultRecords() { parent::requireDefaultRecords(); // make sure to migrate the class across (prior to v5.x) DB::query("UPDATE \"UserDefinedForm_EmailRecipient\" SET \"FormClass\" = 'Page' WHERE \"FormClass\" IS NULL"); } public function onBeforeWrite() { parent::onBeforeWrite(); // email addresses have trim() applied to them during validation for a slightly nicer UX // apply trim() here too before saving to the database $this->EmailAddress = trim($this->EmailAddress); $this->EmailFrom = trim($this->EmailFrom); $this->EmailReplyTo = trim($this->EmailReplyTo); } public function summaryFields() { $fields = parent::summaryFields(); if (isset($fields['EmailAddress'])) { /** @skipUpgrade */ $fields['EmailAddress'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILADDRESS', 'Email'); } if (isset($fields['EmailSubject'])) { $fields['EmailSubject'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Subject'); } if (isset($fields['EmailFrom'])) { $fields['EmailFrom'] = _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILFROM', 'From'); } return $fields; } /** * Get instance of UserForm when editing in getCMSFields * * @return UserDefinedForm|UserForm|null */ protected function getFormParent() { // If polymorphic relationship is actually defined, use it if ($this->FormID && $this->FormClass) { $formClass = $this->FormClass; return $formClass::get()->byID($this->FormID); } // Revert to checking for a form from the session // LeftAndMain::sessionNamespace is protected. @todo replace this with a non-deprecated equivalent. $sessionNamespace = $this->config()->get('session_namespace') ?: CMSMain::class; $formID = Controller::curr()->getRequest()->getSession()->get($sessionNamespace . '.currentPage'); if ($formID) { return UserDefinedForm::get()->byID($formID); } } public function getTitle() { if ($this->EmailAddress) { return $this->EmailAddress; } if ($this->EmailSubject) { return $this->EmailSubject; } return parent::getTitle(); } /** * Generate a gridfield config for editing filter rules * * @return GridFieldConfig */ protected function getRulesConfig() { if (!$this->getFormParent()) { return null; } $formFields = $this->getFormParent()->Fields(); $config = GridFieldConfig::create() ->addComponents( new GridFieldButtonRow('before'), new GridFieldToolbarHeader(), new GridFieldAddNewInlineButton(), new GridFieldDeleteAction(), $columns = new GridFieldEditableColumns() ); $columns->setDisplayFields(array( 'ConditionFieldID' => function ($record, $column, $grid) use ($formFields) { return DropdownField::create($column, false, $formFields->map('ID', 'Title')); }, 'ConditionOption' => function ($record, $column, $grid) { $options = EmailRecipientCondition::config()->condition_options; return DropdownField::create($column, false, $options); }, 'ConditionValue' => function ($record, $column, $grid) { return TextField::create($column); } )); return $config; } /** * @return FieldList */ public function getCMSFields() { Requirements::javascript('silverstripe/userforms:client/dist/js/userforms-cms.js'); // Build fieldlist $fields = FieldList::create(Tabset::create('Root')->addExtraClass('EmailRecipientForm')); if (!$this->getFormParent()) { $fields->addFieldToTab('Root.EmailDetails', $this->getUnsavedFormLiteralField()); } // Configuration fields $fields->addFieldsToTab('Root.EmailDetails', [ $this->getSubjectCMSFields(), $this->getEmailToCMSFields(), $this->getEmailFromCMSFields(), $this->getEmailReplyToCMSFields(), ]); $fields->fieldByName('Root.EmailDetails')->setTitle(_t(__CLASS__ . '.EMAILDETAILSTAB', 'Email Details')); // Only show the preview link if the recipient has been saved. if (!empty($this->EmailTemplate)) { $request = Controller::curr()->getRequest(); $pageEditController = singleton(CMSPageEditController::class); $pageEditController->getRequest()->setSession($request->getSession()); $currentUrl = $request->getURL(); // If used in a regular page context, will have "/edit" on the end, if used in a trait context // it won't. Strip that off in case. It may also have "ItemEditForm" on the end instead if this is // an AJAX request, e.g. saving a GridFieldDetailForm $remove = ['/edit', '/ItemEditForm']; foreach ($remove as $badSuffix) { $badSuffixLength = strlen($badSuffix); if (substr($currentUrl, -$badSuffixLength) === $badSuffix) { $currentUrl = substr($currentUrl, 0, -$badSuffixLength); } } $previewUrl = Controller::join_links($currentUrl, 'preview'); $preview = sprintf( '

%s

%s', $previewUrl, _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL', 'Preview email'), _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_DESCRIPTION', 'Note: Unsaved changes will not appear in the preview.' ) ); } else { $preview = sprintf( '

%s

', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.PREVIEW_EMAIL_UNAVAILABLE', 'You can preview this email once you have saved the Recipient.' ) ); } // Email templates $fields->addFieldsToTab('Root.EmailContent', [ CheckboxField::create( 'HideFormData', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.HIDEFORMDATA', 'Hide form data from email?') ), CheckboxField::create( 'HideInvisibleFields', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.HIDEINVISIBLEFIELDS', 'Hide invisible fields from email?') ), CheckboxField::create( 'SendPlain', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDPLAIN', 'Send email as plain text? (HTML will be stripped)' ) ), HTMLEditorField::create( 'EmailBodyHtml', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODYHTML', 'Body') ) ->addExtraClass('toggle-html-only'), TextareaField::create( 'EmailBody', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILBODY', 'Body') ) ->addExtraClass('toggle-plain-only'), LiteralField::create('EmailPreview', $preview) ]); $templates = $this->getEmailTemplateDropdownValues(); if ($templates) { $fields->insertBefore( DropdownField::create( 'EmailTemplate', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILTEMPLATE', 'Email template'), $templates )->addExtraClass('toggle-html-only'), 'EmailBodyHtml' ); } $fields->fieldByName('Root.EmailContent')->setTitle(_t(__CLASS__ . '.EMAILCONTENTTAB', 'Email Content')); // Custom rules for sending this field $grid = GridField::create( 'CustomRules', _t('SilverStripe\\UserForms\\Model\\EditableFormField.CUSTOMRULES', 'Custom Rules'), $this->CustomRules(), $this->getRulesConfig() ); $grid->setDescription(_t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.RulesDescription', 'Emails will only be sent to the recipient if the custom rules are met. If no rules are defined, ' . 'this recipient will receive notifications for every submission.' )); $fields->addFieldsToTab('Root.CustomRules', [ DropdownField::create( 'CustomRulesCondition', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIF', 'Send condition'), [ 'Or' => _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFOR', 'Any conditions are true' ), 'And' => _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDIFAND', 'All conditions are true' ) ] ), $grid ]); $fields->fieldByName('Root.CustomRules')->setTitle(_t(__CLASS__ . '.CUSTOMRULESTAB', 'Custom Rules')); $this->extend('updateCMSFields', $fields); return $fields; } /** * Return whether a user can create an object of this type * * @param Member $member * @param array $context Virtual parameter to allow context to be passed in to check * @return bool */ public function canCreate($member = null, $context = []) { // Check parent page $parent = $this->getCanCreateContext(func_get_args()); if ($parent) { return $parent->canEdit($member); } // Fall back to secure admin permissions return parent::canCreate($member); } /** * Helper method to check the parent for this object * * @param array $args List of arguments passed to canCreate * @return SiteTree Parent page instance */ protected function getCanCreateContext($args) { // Inspect second parameter to canCreate for a 'Parent' context if (isset($args[1][Form::class])) { return $args[1][Form::class]; } // Hack in currently edited page if context is missing if (Controller::has_curr() && Controller::curr() instanceof CMSMain) { return Controller::curr()->currentPage(); } // No page being edited return null; } public function canView($member = null) { if ($form = $this->getFormParent()) { return $form->canView($member); } return parent::canView($member); } public function canEdit($member = null) { if ($form = $this->getFormParent()) { return $form->canEdit($member); } return parent::canEdit($member); } /** * @param Member * * @return boolean */ public function canDelete($member = null) { return $this->canEdit($member); } /** * Determine if this recipient may receive notifications for this submission * * @param array $data * @param Form $form * @return bool */ public function canSend($data, $form) { // Skip if no rules configured $customRules = $this->CustomRules(); if (!$customRules->count()) { return true; } // Check all rules $isAnd = $this->CustomRulesCondition === 'And'; foreach ($customRules as $customRule) { /** @var EmailRecipientCondition $customRule */ $matches = $customRule->matches($data); if ($isAnd && !$matches) { return false; } if (!$isAnd && $matches) { return true; } } // Once all rules are checked return $isAnd; } /** * Make sure the email template saved against the recipient exists on the file system. * * @param string * * @return boolean */ public function emailTemplateExists($template = '') { $t = ($template ? $template : $this->EmailTemplate); return array_key_exists($t, (array) $this->getEmailTemplateDropdownValues()); } /** * Get the email body for the current email format * * @return string */ public function getEmailBodyContent() { if ($this->SendPlain) { return DBField::create_field('HTMLText', $this->EmailBody)->Plain(); } return DBField::create_field('HTMLText', $this->EmailBodyHtml); } /** * Gets a list of email templates suitable for populating the email template dropdown. * * @return array */ public function getEmailTemplateDropdownValues() { $templates = []; $finder = new FileFinder(); $finder->setOption('name_regex', '/^.*\.ss$/'); $parent = $this->getFormParent(); if (!$parent) { return []; } $emailTemplateDirectory = $parent->config()->get('email_template_directory'); $templateDirectory = ModuleResourceLoader::resourcePath($emailTemplateDirectory); if (!$templateDirectory) { return []; } $found = $finder->find(BASE_PATH . DIRECTORY_SEPARATOR . $templateDirectory); foreach ($found as $key => $value) { $template = pathinfo($value); $absoluteFilename = $template['dirname'] . DIRECTORY_SEPARATOR . $template['filename']; // Optionally remove vendor/ path prefixes $resource = ModuleResourceLoader::singleton()->resolveResource($emailTemplateDirectory); if ($resource instanceof ModuleResource && $resource->getModule()) { $prefixToStrip = $resource->getModule()->getPath(); } else { $prefixToStrip = BASE_PATH; } $templatePath = substr($absoluteFilename, strlen($prefixToStrip) + 1); // Optionally remove "templates/" ("templates\" on Windows respectively) prefixes if (preg_match('#(?<=templates' . preg_quote(DIRECTORY_SEPARATOR, '#') . ').*$#', $templatePath, $matches)) { $templatePath = $matches[0]; } $templates[$templatePath] = $template['filename']; } return $templates; } /** * Validate that valid email addresses are being used * * @return ValidationResult */ public function validate() { $result = parent::validate(); $checkEmail = [ 'EmailAddress' => 'EMAILADDRESSINVALID', 'EmailFrom' => 'EMAILFROMINVALID', 'EmailReplyTo' => 'EMAILREPLYTOINVALID', ]; foreach ($checkEmail as $check => $translation) { if ($this->$check) { //may be a comma separated list of emails $addresses = explode(',', $this->$check); foreach ($addresses as $address) { $trimAddress = trim($address); if ($trimAddress && !Email::is_valid_address($trimAddress)) { $error = _t( __CLASS__.".$translation", "Invalid email address $trimAddress" ); $result->addError($error . " ($trimAddress)"); } } } } // if there is no from address and no fallback, you'll have errors if this isn't defined if (!$this->EmailFrom && empty(Email::getSendAllEmailsFrom()) && empty(Email::config()->get('admin_email'))) { $result->addError(_t(__CLASS__.".EMAILFROMREQUIRED", '"Email From" address is required')); } // Sending will also fail if there's no recipient defined if (!$this->EmailAddress && !$this->SendEmailToFieldID) { $result->addError(_t(__CLASS__.".EMAILTOREQUIRED", '"Send email to" address or field is required')); } return $result; } /** * @return FieldGroup|TextField */ protected function getSubjectCMSFields() { $subjectTextField = TextField::create( 'EmailSubject', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPESUBJECT', 'Type subject') ) ->setAttribute('style', 'min-width: 400px;'); if ($this->getFormParent() && $this->getValidSubjectFields()) { return FieldGroup::create( $subjectTextField, DropdownField::create( 'SendEmailSubjectFieldID', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.SELECTAFIELDTOSETSUBJECT', '.. or select a field to use as the subject' ), $this->getValidSubjectFields()->map('ID', 'Title') )->setEmptyString('') ) ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAILSUBJECT', 'Email subject')); } else { return $subjectTextField; } } /** * @return FieldGroup|TextField */ protected function getEmailToCMSFields() { $emailToTextField = TextField::create( 'EmailAddress', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPETO', 'Type to address') ) ->setAttribute('style', 'min-width: 400px;'); if ($this->getFormParent() && $this->getValidEmailToFields()) { return FieldGroup::create( $emailToTextField, DropdownField::create( 'SendEmailToFieldID', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASTO', '.. or select a field to use as the to address' ), $this->getValidEmailToFields()->map('ID', 'Title') )->setEmptyString(' ') ) ->setTitle(_t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO', 'Send email to')) ->setDescription(_t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.SENDEMAILTO_DESCRIPTION', 'You may enter multiple email addresses as a comma separated list.' )); } else { return $emailToTextField; } } /** * @return TextField */ protected function getEmailFromCMSFields() { return TextField::create( 'EmailFrom', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.FROMADDRESS', 'Send email from') ) ->setDescription(_t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.EmailFromContent', "The from address allows you to set who the email comes from. On most servers this " . "will need to be set to an email address on the same domain name as your site. " . "For example on yoursite.com the from address may need to be something@yoursite.com. " . "You can however, set any email address you wish as the reply to address." )); } /** * @return FieldGroup|TextField */ protected function getEmailReplyToCMSFields() { $replyToTextField = TextField::create('EmailReplyTo', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.TYPEREPLY', 'Type reply address' )) ->setAttribute('style', 'min-width: 400px;'); if ($this->getFormParent() && $this->getValidEmailFromFields()) { return FieldGroup::create( $replyToTextField, DropdownField::create( 'SendEmailFromFieldID', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.ORSELECTAFIELDTOUSEASFROM', '.. or select a field to use as reply to address' ), $this->getValidEmailFromFields()->map('ID', 'Title') )->setEmptyString(' ') ) ->setTitle(_t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS', 'Email for reply to' )) ->setDescription(_t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.REPLYADDRESS_DESCRIPTION', 'The email address which the recipient is able to \'reply\' to.' )); } else { return $replyToTextField; } } /** * @return DataList|null */ protected function getMultiOptionFields() { if (!$form = $this->getFormParent()) { return null; } return EditableMultipleOptionField::get()->filter('ParentID', $form->ID); } /** * @return ArrayList|null */ protected function getValidSubjectFields() { if (!$form = $this->getFormParent()) { return null; } // For the subject, only one-line entry boxes make sense $validSubjectFields = ArrayList::create( EditableTextField::get() ->filter('ParentID', $form->ID) ->exclude('Rows:GreaterThan', 1) ->toArray() ); $validSubjectFields->merge($this->getMultiOptionFields()); return $validSubjectFields; } /** * @return DataList|null */ protected function getValidEmailFromFields() { if (!$form = $this->getFormParent()) { return null; } // if they have email fields then we could send from it return EditableEmailField::get()->filter('ParentID', $form->ID); } /** * @return ArrayList|DataList|null */ protected function getValidEmailToFields() { if (!$this->getFormParent()) { return null; } // Check valid email-recipient fields if ($this->config()->get('allow_unbound_recipient_fields')) { // To address can only be email fields or multi option fields $validEmailToFields = ArrayList::create($this->getValidEmailFromFields()->toArray()); $validEmailToFields->merge($this->getMultiOptionFields()); return $validEmailToFields; } else { // To address cannot be unbound, so restrict to pre-defined lists return $this->getMultiOptionFields(); } } protected function getUnsavedFormLiteralField() { return LiteralField::create( 'UnsavedFormMessage', sprintf( '

%s

', _t( 'SilverStripe\\UserForms\\Model\\UserDefinedForm.EMAIL_RECIPIENT_UNSAVED_FORM', 'You will be able to select from valid form fields after saving this record.' ) ) ); } }