data(); // load the css if (!$page->config()->get('block_default_userforms_css')) { Requirements::css('silverstripe/userforms:client/dist/styles/userforms.css'); } // load the jquery if (!$page->config()->get('block_default_userforms_js')) { Requirements::javascript('silverstripe/userforms:client/dist/js/jquery.min.js'); Requirements::javascript( 'silverstripe/userforms:client/dist/js/jquery-validation/jquery.validate.min.js' ); Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js'); Requirements::add_i18n_javascript('silverstripe/userforms:client/lang'); Requirements::javascript('silverstripe/userforms:client/dist/js/userforms.js'); $this->addUserFormsValidatei18n(); // Bind a confirmation message when navigating away from a partially completed form. if ($page::config()->get('enable_are_you_sure')) { Requirements::javascript( 'silverstripe/userforms:client/dist/js/jquery.are-you-sure/jquery.are-you-sure.js' ); } } } /** * Add the necessary jQuery validate i18n translation files, either by locale or by langauge, * e.g. 'en_NZ' or 'en'. This adds "methods_abc.min.js" as well as "messages_abc.min.js" from the * jQuery validate thirdparty library from dist/js. */ protected function addUserFormsValidatei18n() { $module = ModuleLoader::getModule('silverstripe/userforms'); $candidates = [ i18n::getData()->langFromLocale(i18n::config()->get('default_locale')), i18n::config()->get('default_locale'), i18n::getData()->langFromLocale(i18n::get_locale()), i18n::get_locale(), ]; foreach ($candidates as $candidate) { foreach (['messages', 'methods'] as $candidateType) { $localisationCandidate = "client/dist/js/jquery-validation/localization/{$candidateType}_{$candidate}.min.js"; $resource = $module->getResource($localisationCandidate); if ($resource->exists()) { Requirements::javascript($resource->getRelativePath()); } } } } /** * Using $UserDefinedForm in the Content area of the page shows * where the form should be rendered into. If it does not exist * then default back to $Form. * * @return array */ public function index(HTTPRequest $request = null) { $form = $this->Form(); if ($this->Content && $form && !$this->config()->disable_form_content_shortcode) { $hasLocation = stristr($this->Content ?? '', '$UserDefinedForm'); if ($hasLocation) { /** @see Requirements_Backend::escapeReplacement */ $formEscapedForRegex = addcslashes($form->forTemplate() ?? '', '\\$'); $content = preg_replace( '/(]*>)?\\$UserDefinedForm(<\\/p>)?/i', $formEscapedForRegex ?? '', $this->Content ?? '' ); return [ 'Content' => DBField::create_field('HTMLText', $content), 'Form' => '' ]; } } return [ 'Content' => DBField::create_field('HTMLText', $this->Content), 'Form' => $this->Form() ]; } /** * Keep the session alive for the user. * * @return int */ public function ping() { return 1; } /** * Get the form for the page. Form can be modified by calling {@link updateForm()} * on a UserDefinedForm extension. * * @return Form */ public function Form() { $form = UserForm::create($this, 'Form_' . $this->ID); $form->setFormAction(Controller::join_links($this->Link(), 'Form')); $this->generateConditionalJavascript(); return $form; } /** * Generate the javascript for the conditional field show / hiding logic. * * @return void */ public function generateConditionalJavascript() { $rules = ''; $form = $this->data(); if (!$form) { return; } $formFields = $form->Fields(); $watch = []; if ($formFields) { /** @var EditableFormField $field */ foreach ($formFields as $field) { if ($result = $field->formatDisplayRules()) { $watch[] = $result; } } } if ($watch) { $rules .= $this->buildWatchJS($watch); } // Only add customScript if $default or $rules is defined if ($rules) { Requirements::customScript(<<ID); } } /** * Returns the maximum size uploaded files can be before they're excluded from CMS configured recipient emails * * @return int size in megabytes */ public function getMaximumAllowedEmailAttachmentSize() { return $this->parseByteSizeString($this->config()->get('maximum_email_attachment_size')); } /** * Convert file sizes with a single character for unit size to true byte count. * Just as with php.ini and e.g. 128M -> 1024 * 1024 * 128 bytes. * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes * * @param string $byteSize * @return int bytes */ protected function parseByteSizeString($byteSize) { // kilo, mega, giga $validUnits = 'kmg'; $valid = preg_match("/^(?\d+)((?[$validUnits])b?)?$/i", $byteSize, $matches); if (!$valid) { throw new \InvalidArgumentException( "Expected a positive integer followed optionally by K, M, or G. Found '$byteSize' instead" ); } $power = 0; // prepend b for bytes to $validUnits to give correct mapping of ordinal position to exponent if (isset($matches['unit'])) { $power = stripos("b$validUnits", $matches['unit']); } return intval($matches['number']) * pow(1024, $power); } /** * Process the form that is submitted through the site * * {@see UserForm::validate()} for validation step prior to processing * * @param array $data * @param Form $form * * @return HTTPResponse */ public function process($data, $form) { $submittedForm = SubmittedForm::create(); $submittedForm->SubmittedByID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0; $submittedForm->ParentClass = get_class($this->data()); $submittedForm->ParentID = $this->ID; // if saving is not disabled save now to generate the ID if (!$this->DisableSaveSubmissions) { $submittedForm->write(); } $attachments = []; $submittedFields = ArrayList::create(); foreach ($this->data()->Fields() as $field) { if (!$field->showInReports()) { continue; } $submittedField = $field->getSubmittedFormField(); $submittedField->ParentID = $submittedForm->ID; $submittedField->Name = $field->Name; $submittedField->Title = $field->getField('Title'); // save the value from the data if ($field->hasMethod('getValueFromData')) { $submittedField->Value = $field->getValueFromData($data); } else { if (isset($data[$field->Name])) { $submittedField->Value = $data[$field->Name]; } } // set visibility flag according to display rules $submittedField->Displayed = $field->isDisplayed($data); if (!empty($data[$field->Name])) { if (in_array(EditableFileField::class, $field->getClassAncestry() ?? [])) { if (!empty($_FILES[$field->Name]['name'])) { if (!$field->getFolderExists()) { $field->createProtectedFolder(); } $file = Versioned::withVersionedMode(function () use ($field, $form) { $stage = Injector::inst()->get(self::class)->config()->get('file_upload_stage'); Versioned::set_stage($stage); $foldername = $field->getFormField()->getFolderName(); // create the file from post data $upload = Upload::create(); try { $upload->loadIntoFile($_FILES[$field->Name], null, $foldername); } catch (ValidationException $e) { $validationResult = $e->getResult(); foreach ($validationResult->getMessages() as $message) { $form->sessionMessage($message['message'], ValidationResult::TYPE_ERROR); } Controller::curr()->redirectBack(); return null; } /** @var AssetContainer|File $file */ $file = $upload->getFile(); $file->ShowInSearch = 0; $file->UserFormUpload = UserFormFileExtension::USER_FORM_UPLOAD_TRUE; $file->write(); return $file; }); if (is_null($file)) { return; } // generate image thumbnail to show in asset-admin // you can run userforms without asset-admin, so need to ensure asset-admin is installed if (class_exists(AssetAdmin::class)) { AssetAdmin::singleton()->generateThumbnails($file); } // write file to form field $submittedField->UploadedFileID = $file->ID; // attach a file to recipient email only if lower than configured size if ($file->getAbsoluteSize() <= $this->getMaximumAllowedEmailAttachmentSize()) { $attachments[] = $file; } } } } $submittedField->extend('onPopulationFromField', $field); if (!$this->DisableSaveSubmissions) { $submittedField->write(); } $submittedFields->push($submittedField); } $visibleSubmittedFields = $submittedFields->filter('Displayed', true); $emailData = [ 'Sender' => Security::getCurrentUser(), 'HideFormData' => false, 'SubmittedForm' => $submittedForm, 'Fields' => $submittedFields, 'Body' => '', ]; $this->extend('updateEmailData', $emailData, $attachments); // email users on submit. if ($recipients = $this->FilteredEmailRecipients($data, $form)) { foreach ($recipients as $recipient) { $email = Email::create() ->setHTMLTemplate('email/SubmittedFormEmail') ->setPlainTemplate('email/SubmittedFormEmailPlain'); // Merge fields are used for CMS authors to reference specific form fields in email content $mergeFields = $this->getMergeFieldsMap($emailData['Fields']); if ($attachments && (bool) $recipient->HideFormData === false) { foreach ($attachments as $file) { /** @var File $file */ if ((int) $file->ID === 0) { continue; } $email->addAttachmentFromData( $file->getString(), $file->getFilename(), $file->getMimeType() ); } } if (!$recipient->SendPlain && $recipient->emailTemplateExists()) { $email->setHTMLTemplate($recipient->EmailTemplate); } // Add specific template data for the current recipient $emailData['HideFormData'] = (bool) $recipient->HideFormData; // Include any parsed merge field references from the CMS editor - this is already escaped // This string substitution works for both HTML and plain text emails. // $recipient->getEmailBodyContent() will retrieve the relevant version of the email $emailData['Body'] = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields); // only include visible fields if recipient visibility flag is set if ((bool) $recipient->HideInvisibleFields) { $emailData['Fields'] = $visibleSubmittedFields; } // Push the template data to the Email's data foreach ($emailData as $key => $value) { $email->addData($key, $value); } // check to see if they are a dynamic reply to. eg based on a email field a user selected $emailFrom = $recipient->SendEmailFromField(); if ($emailFrom && $emailFrom->exists()) { $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailFromField()->Name); if ($submittedFormField && $submittedFormField->Value && is_string($submittedFormField->Value)) { $emailSendTo = $this->validEmailsToArray($submittedFormField->Value); $email->addReplyTo(...$emailSendTo); } } elseif ($recipient->EmailReplyTo) { $emailReplyTo = $this->validEmailsToArray($recipient->EmailReplyTo); $email->addReplyTo(...$emailReplyTo); } // check for a specified from; otherwise fall back to server defaults if ($recipient->EmailFrom) { $email->setFrom($this->validEmailsToArray($recipient->EmailFrom)); } // check to see if they are a dynamic reciever eg based on a dropdown field a user selected $emailTo = $recipient->SendEmailToField(); try { if ($emailTo && $emailTo->exists()) { $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailToField()->Name); if ($submittedFormField && is_string($submittedFormField->Value)) { $email->setTo($this->validEmailsToArray($submittedFormField->Value)); } else { $email->setTo($this->validEmailsToArray($recipient->EmailAddress)); } } else { $email->setTo($this->validEmailsToArray($recipient->EmailAddress)); } } catch (Swift_RfcComplianceException $e) { // The sending address is empty and/or invalid. Log and skip sending. $error = sprintf( 'Failed to set sender for userform submission %s: %s', $submittedForm->ID, $e->getMessage() ); Injector::inst()->get(LoggerInterface::class)->notice($error); continue; } // check to see if there is a dynamic subject $emailSubject = $recipient->SendEmailSubjectField(); if ($emailSubject && $emailSubject->exists()) { $submittedFormField = $submittedFields->find('Name', $recipient->SendEmailSubjectField()->Name); if ($submittedFormField && trim($submittedFormField->Value ?? '')) { $email->setSubject($submittedFormField->Value); } else { $email->setSubject(SSViewer::execute_string($recipient->EmailSubject, $mergeFields)); } } else { $email->setSubject(SSViewer::execute_string($recipient->EmailSubject, $mergeFields)); } $this->extend('updateEmail', $email, $recipient, $emailData); if ((bool)$recipient->SendPlain) { // decode previously encoded html tags because the email is being sent as text/plain $body = html_entity_decode($emailData['Body'] ?? '') . "\n"; if (isset($emailData['Fields']) && !$emailData['HideFormData']) { foreach ($emailData['Fields'] as $field) { if ($field instanceof SubmittedFileField) { $body .= $field->Title . ': ' . $field->ExportValue ." \n"; } else { $body .= $field->Title . ': ' . $field->Value . " \n"; } } } $email->setBody($body); try { $email->sendPlain(); } catch (Exception $e) { Injector::inst()->get(LoggerInterface::class)->error($e); } } else { try { $email->send(); } catch (Exception $e) { Injector::inst()->get(LoggerInterface::class)->error($e); } } } } $submittedForm->extend('updateAfterProcess', $emailData, $attachments); $session = $this->getRequest()->getSession(); $session->clear("FormInfo.{$form->FormName()}.errors"); $session->clear("FormInfo.{$form->FormName()}.data"); $referrer = (isset($data['Referrer'])) ? '?referrer=' . urlencode($data['Referrer']) : ""; // set a session variable from the security ID to stop people accessing // the finished method directly. if (!$this->DisableAuthenicatedFinishAction) { if (isset($data['SecurityID'])) { $session->set('FormProcessed', $data['SecurityID']); } else { // if the form has had tokens disabled we still need to set FormProcessed // to allow us to get through the finshed method if (!$this->Form()->getSecurityToken()->isEnabled()) { $randNum = rand(1, 1000); $randHash = md5($randNum ?? ''); $session->set('FormProcessed', $randHash); $session->set('FormProcessedNum', $randNum); } } } if (!$this->DisableSaveSubmissions) { $session->set('userformssubmission'. $this->ID, $submittedForm->ID); } return $this->redirect($this->Link('finished') . $referrer . $this->config()->get('finished_anchor')); } /** * Allows the use of field values in email body. * * @param ArrayList $fields * @return ArrayData */ protected function getMergeFieldsMap($fields = []) { $data = ArrayData::create([]); foreach ($fields as $field) { $data->setField($field->Name, DBField::create_field('Text', $field->Value)); } return $data; } /** * This action handles rendering the "finished" message, which is * customizable by editing the ReceivedFormSubmission template. * * @return ViewableData */ public function finished() { $submission = $this->getRequest()->getSession()->get('userformssubmission'. $this->ID); if ($submission) { $submission = SubmittedForm::get()->byId($submission); } $referrer = isset($_GET['referrer']) ? urldecode($_GET['referrer']) : null; if (!$this->DisableAuthenicatedFinishAction) { $formProcessed = $this->getRequest()->getSession()->get('FormProcessed'); if (!isset($formProcessed)) { return $this->redirect($this->Link() . $referrer); } else { $securityID = $this->getRequest()->getSession()->get('SecurityID'); // make sure the session matches the SecurityID and is not left over from another form if ($formProcessed != $securityID) { // they may have disabled tokens on the form $securityID = md5($this->getRequest()->getSession()->get('FormProcessedNum') ?? ''); if ($formProcessed != $securityID) { return $this->redirect($this->Link() . $referrer); } } } $this->getRequest()->getSession()->clear('FormProcessed'); } $data = [ 'Submission' => $submission, 'Link' => $referrer ]; $this->extend('updateReceivedFormSubmissionData', $data); return $this->customise([ 'Content' => $this->customise($data)->renderWith(__CLASS__ . '_ReceivedFormSubmission'), 'Form' => '', ]); } /** * Outputs the required JS from the $watch input * * @param array $watch * * @return string */ protected function buildWatchJS($watch) { $result = ''; foreach ($watch as $key => $rule) { $events = implode(' ', $rule['events']); $selectors = implode(', ', $rule['selectors']); $conjunction = $rule['conjunction']; $operations = implode(" {$conjunction} ", $rule['operations']); $target = $rule['targetFieldID']; $holder = $rule['holder']; $isFormStep = strpos($target ?? '', 'EditableFormStep') !== false; $result .= <<