From 71e527fb7fff45905c4ad35d69977b30017b042f Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 21 Jun 2017 11:52:19 +0200 Subject: [PATCH] Added frontend contact form See #2308 --- com.woltlab.wcf/menuItem.xml | 6 + com.woltlab.wcf/page.xml | 15 ++ com.woltlab.wcf/templates/contact.tpl | 89 +++++++++ .../templates/customOptionFieldList.tpl | 19 ++ com.woltlab.wcf/templates/email_contact.tpl | 9 + .../option/ContactOptionAction.class.php | 52 +++++ .../option/ContactOptionEditor.class.php | 11 +- .../recipient/ContactRecipient.class.php | 3 +- .../data/custom/option/CustomOption.class.php | 26 ++- .../files/lib/form/ContactForm.class.php | 177 ++++++++++++++++++ .../ContactOptionCacheBuilder.class.php | 24 +++ .../option/ContactOptionHandler.class.php | 12 ++ .../option/CustomOptionHandler.class.php | 112 +++++++++++ wcfsetup/install/files/style/layout/form.scss | 4 + wcfsetup/install/lang/de.xml | 21 +++ wcfsetup/install/lang/en.xml | 28 +++ 16 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 com.woltlab.wcf/templates/contact.tpl create mode 100644 com.woltlab.wcf/templates/customOptionFieldList.tpl create mode 100644 com.woltlab.wcf/templates/email_contact.tpl create mode 100644 wcfsetup/install/files/lib/form/ContactForm.class.php create mode 100644 wcfsetup/install/files/lib/system/cache/builder/ContactOptionCacheBuilder.class.php create mode 100644 wcfsetup/install/files/lib/system/option/ContactOptionHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php diff --git a/com.woltlab.wcf/menuItem.xml b/com.woltlab.wcf/menuItem.xml index a78e55a272..7944df241c 100644 --- a/com.woltlab.wcf/menuItem.xml +++ b/com.woltlab.wcf/menuItem.xml @@ -51,5 +51,11 @@ Privacy Policy com.woltlab.wcf.PrivacyPolicy + + com.woltlab.wcf.FooterMenu + Kontakt + Contact + com.woltlab.wcf.Contact + diff --git a/com.woltlab.wcf/page.xml b/com.woltlab.wcf/page.xml index 0244211050..a9c03d5d63 100644 --- a/com.woltlab.wcf/page.xml +++ b/com.woltlab.wcf/page.xml @@ -534,6 +534,21 @@ 1 1 + + system + wcf\form\ContactForm + Kontakt-Formular + Contact Form + module_contact_form + 0 + + + Contact + + + Kontakt + + diff --git a/com.woltlab.wcf/templates/contact.tpl b/com.woltlab.wcf/templates/contact.tpl new file mode 100644 index 0000000000..b29fbea4c5 --- /dev/null +++ b/com.woltlab.wcf/templates/contact.tpl @@ -0,0 +1,89 @@ +{include file='header'} + +{include file='formError'} + +
+
+

{lang}wcf.contact.sender.information{/lang}

+ + +
+
+ + {if $errorField == 'name'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.contact.sender.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + + +
+
+ + {if $errorField == 'email'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.user.email.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + + {event name='informationFields'} +
+ +
+

{lang}wcf.contact.data{/lang}

+ + {if $recipientList|count > 1} + +
+
+ + {if $errorField == 'recipientID'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.contact.recipientID.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + {/if} + + {include file='customOptionFieldList'} + + {event name='optionFields'} +
+ + {event name='sections'} + + {include file='captcha' supportsAsyncCaptcha=true} + +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+ +
+

* {lang}wcf.contact.options.required{/lang}

+
+
+ +{include file='footer'} diff --git a/com.woltlab.wcf/templates/customOptionFieldList.tpl b/com.woltlab.wcf/templates/customOptionFieldList.tpl new file mode 100644 index 0000000000..23fb04c808 --- /dev/null +++ b/com.woltlab.wcf/templates/customOptionFieldList.tpl @@ -0,0 +1,19 @@ +{foreach from=$options item=optionData} + {assign var=option value=$optionData[object]} +
+ {if $option->required} *{/if} +
{@$optionData[html]} + {lang __optional=true}{$option->optionDescription}{/lang} + + {if $errorType|is_array && $errorType[$option->optionName]|isset} + + {if $errorType[$option->optionName] == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.customOption.error.{$errorType[$option->optionName]}{/lang} + {/if} + + {/if} +
+
+{/foreach} diff --git a/com.woltlab.wcf/templates/email_contact.tpl b/com.woltlab.wcf/templates/email_contact.tpl new file mode 100644 index 0000000000..40335f9175 --- /dev/null +++ b/com.woltlab.wcf/templates/email_contact.tpl @@ -0,0 +1,9 @@ +{if $mimeType === 'text/plain'} +{capture assign='content'}{lang}wcf.contact.mail.plaintext{/lang}{/capture} +{include file='email_plaintext'} +{else} + {capture assign='content'} + {lang}wcf.contact.mail.html{/lang} + {/capture} + {include file='email_html'} +{/if} diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php index ce7d64058a..5686bfe526 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php @@ -1,6 +1,14 @@ getDefaultLanguage(); + + $recipient = new ContactRecipient($this->parameters['recipientID']); + /** @var ContactOptionHandler $optionHandler */ + $optionHandler = $this->parameters['optionHandler']; + + $options = []; + foreach ($optionHandler->getOptions() as $option) { + /** @var ContactOption $object */ + $object = $option['object']; + $options[] = [ + 'isMessage' => $object->isMessage(), + 'title' => $object->getLocalizedName($defaultLanguage), + 'value' => $object->getFormattedOptionValue() + ]; + } + + // build message data + $messageData = [ + 'options' => $options, + 'recipient' => $recipient, + 'name' => $this->parameters['name'] + ]; + + // build mail + $email = new Email(); + $email->addRecipient(new Mailbox($recipient->email)); + $email->setSubject($defaultLanguage->get('wcf.contact.mail.subject')); + $email->setBody(new MimePartFacade([ + new RecipientAwareTextMimePart('text/html', 'email_contact', 'wcf', $messageData), + new RecipientAwareTextMimePart('text/plain', 'email_contact', 'wcf', $messageData) + ])); + + // add reply-to tag + $email->setReplyTo(new Mailbox($this->parameters['email'])); + + // send mail + $email->send(); + } } diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php index 7f314fdbfe..29276f724a 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php @@ -1,6 +1,8 @@ reset(); + } } diff --git a/wcfsetup/install/files/lib/data/contact/recipient/ContactRecipient.class.php b/wcfsetup/install/files/lib/data/contact/recipient/ContactRecipient.class.php index cb78bc7865..9ce5913845 100644 --- a/wcfsetup/install/files/lib/data/contact/recipient/ContactRecipient.class.php +++ b/wcfsetup/install/files/lib/data/contact/recipient/ContactRecipient.class.php @@ -1,6 +1,7 @@ name; + return WCF::getLanguage()->get($this->name); } /** diff --git a/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php b/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php index efeb6646fb..1ec04892f5 100644 --- a/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php +++ b/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php @@ -1,5 +1,6 @@ optionValue = $value; } + /** + * Attempts to return the localized option name. + * + * @param Language $language + * @return string + */ + public function getLocalizedName(Language $language) { + if (preg_match('~^wcf\.contact\.option\d+$~', $this->optionTitle)) { + return $language->get($this->optionTitle); + } + + return $this->optionTitle; + } + /** * Returns the formatted value of this option. * @@ -79,7 +94,7 @@ abstract class CustomOption extends Option { public function getFormattedOptionValue() { switch ($this->optionType) { case 'boolean': - return WCF::getLanguage()->get('wcf.acp.option.optionType.boolean.'.($this->optionValue ? 'yes' : 'no')); + return WCF::getLanguage()->get('wcf.acp.customOption.optionType.boolean.'.($this->optionValue ? 'yes' : 'no')); case 'date': $year = $month = $day = 0; @@ -137,4 +152,13 @@ abstract class CustomOption extends Option { public function canDelete() { return !$this->originIsSystem; } + + /** + * Returns true if this option represents a message-type value. + * + * @return boolean + */ + public function isMessage() { + return ($this->optionType === 'textarea' || $this->optionType === 'message'); + } } diff --git a/wcfsetup/install/files/lib/form/ContactForm.class.php b/wcfsetup/install/files/lib/form/ContactForm.class.php new file mode 100644 index 0000000000..5860646331 --- /dev/null +++ b/wcfsetup/install/files/lib/form/ContactForm.class.php @@ -0,0 +1,177 @@ + + * @package WoltLabSuite\Core\Form + */ +class ContactForm extends AbstractCaptchaForm { + public $email = ''; + + public $name = ''; + + /** + * @inheritDoc + */ + public $neededModules = ['MODULE_CONTACT_FORM']; + + /** + * @var ContactOptionHandler + */ + public $optionHandler; + + /** + * recipient id + * @var integer + */ + public $recipientID = 0; + + /** + * @var ContactRecipientList + */ + public $recipientList; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + $this->optionHandler = new ContactOptionHandler(false); + $this->optionHandler->init(); + + $this->recipientList = new ContactRecipientList(); + $this->recipientList->getConditionBuilder()->add("contact_recipient.isDisabled = ?", [0]); + $this->recipientList->readObjects(); + + if (count($this->recipientList) < 0) { + throw new IllegalLinkException(); + } + } + + public function readFormParameters() { + parent::readFormParameters(); + + $this->optionHandler->readUserInput($_POST); + + if (isset($_POST['email'])) $this->email = StringUtil::trim($_POST['email']); + if (isset($_POST['name'])) $this->name = StringUtil::trim($_POST['name']); + if (isset($_POST['recipientID'])) $this->recipientID = intval($_POST['recipientID']); + } + + /** + * @inheritDoc + */ + public function validate() { + // validate file options + $optionHandlerErrors = $this->optionHandler->validate(); + + parent::validate(); + + if (!empty($optionHandlerErrors)) { + throw new UserInputException('options', $optionHandlerErrors); + } + + if (empty($this->email)) { + throw new UserInputException('email'); + } + else { + try { + new Mailbox($this->email); + } + catch (\DomainException $e) { + throw new UserInputException('email', 'invalid'); + } + } + + if (empty($this->name)) { + throw new UserInputException('name'); + } + + $recipients = $this->recipientList->getObjects(); + if (count($recipients) === 1) { + $this->recipientID = reset($recipients)->recipientID; + } + else { + if (!$this->recipientID) { + throw new UserInputException('recipientID'); + } + + $isValid = false; + foreach ($recipients as $recipient) { + if ($this->recipientID == $recipient->recipientID) { + $isValid = true; + break; + } + } + + if (!$isValid) { + throw new UserInputException('recipientID', 'invalid'); + } + } + } + + /** + * @inheritDoc + */ + public function readData() { + parent::readData(); + + if (empty($_POST) && WCF::getUser()->userID) { + $this->email = WCF::getUser()->email; + $this->name = WCF::getUser()->username; + } + } + + /** + * @inheritDoc + */ + public function save() { + parent::save(); + + $this->objectAction = new ContactOptionAction([], 'send', [ + 'email' => $this->email, + 'name' => $this->name, + 'optionHandler' => $this->optionHandler, + 'recipientID' => $this->recipientID + ]); + $this->objectAction->executeAction(); + + // call saved event + $this->saved(); + + HeaderUtil::delayedRedirect( + LinkHandler::getInstance()->getLink(''), + WCF::getLanguage()->get('wcf.contact.success') + ); + exit; + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'email' => $this->email, + 'name' => $this->name, + 'options' => $this->optionHandler->getOptions(), + 'recipientList' => $this->recipientList + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/ContactOptionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/ContactOptionCacheBuilder.class.php new file mode 100644 index 0000000000..f45244f7e7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/ContactOptionCacheBuilder.class.php @@ -0,0 +1,24 @@ + + * @package WoltLabSuite\Core\System\Cache\Builder + */ +class ContactOptionCacheBuilder extends AbstractCacheBuilder { + /** + * @inheritDoc + */ + public function rebuild(array $parameters) { + $list = new ContactOptionList(); + $list->sqlSelects = "CONCAT('contactOption', CAST(contact_option.optionID AS CHAR)) AS optionName"; + $list->readObjects(); + + return $list->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/system/option/ContactOptionHandler.class.php b/wcfsetup/install/files/lib/system/option/ContactOptionHandler.class.php new file mode 100644 index 0000000000..b49dac862d --- /dev/null +++ b/wcfsetup/install/files/lib/system/option/ContactOptionHandler.class.php @@ -0,0 +1,12 @@ +cachedOptions = ContactOptionCacheBuilder::getInstance()->getData(); + } +} diff --git a/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php b/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php new file mode 100644 index 0000000000..948b331755 --- /dev/null +++ b/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php @@ -0,0 +1,112 @@ +cachedOptions = FileOptionCacheBuilder::getInstance()->getData(); + } + + /** + * Initializes active options. + */ + public function init() { + if (!$this->didInit) { + // get active options + foreach ($this->cachedOptions as $option) { + if ($this->checkOption($option)) { + $this->options[$option->optionName] = $option; + } + } + + // mark options as initialized + $this->didInit = true; + } + } + + /** + * Returns the parsed options. + * + * @return array + */ + public function getOptions() { + $parsedOptions = []; + foreach ($this->options as $option) { + $parsedOptions[] = $this->getOption($option->optionName); + } + + return $parsedOptions; + } + + /** + * @inheritDoc + */ + public function readData() { + /** @var CustomOption $option */ + foreach ($this->options as $option) { + if (!isset($this->optionValues[$option->optionName])) { + $this->optionValues[$option->optionName] = $option->defaultValue; + } + } + } + + /** + * Resets the option values. + */ + public function resetOptionValues() { + $this->optionValues = []; + } + + /** + * Returns the option values. + * + * @return array + */ + public function getOptionValues() { + return $this->optionValues; + } + + /** + * Sets the option values. + * + * @param array $values + */ + public function setOptionValues(array $values) { + $this->optionValues = $values; + } + + /** + * @inheritDoc + */ + public function getOption($optionName) { + $optionData = parent::getOption($optionName); + + if (isset($this->optionValues[$optionName])) { + /** @noinspection PhpUndefinedMethodInspection */ + $optionData['object']->setOptionValue($this->optionValues[$optionName]); + } + + return $optionData; + } + + /** + * @inheritDoc + */ + protected function validateOption(Option $option) { + /** @var CustomOption $option */ + + parent::validateOption($option); + + if ($option->required && $option->optionType != 'boolean' && empty($this->optionValues[$option->optionName])) { + throw new UserInputException($option->optionName); + } + } +} diff --git a/wcfsetup/install/files/style/layout/form.scss b/wcfsetup/install/files/style/layout/form.scss index 02b1ae1a77..1a92d632f1 100644 --- a/wcfsetup/install/files/style/layout/form.scss +++ b/wcfsetup/install/files/style/layout/form.scss @@ -275,3 +275,7 @@ input { width: 100%; } } + +.customOptionRequired { + color: rgba(204, 0, 1, 1) !important; +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f89c0e8515..bb10d654cd 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -2341,10 +2341,31 @@ Fehler sind beispielsweise: + + + + Hallo, + +

„{$name}“ hat {if LANGUAGE_USE_INFORMAL_VARIANT}dir{else}Ihnen{/if} über das Kontakt-Formular auf Website {PAGE_TITLE|language} folgende Nachricht gesandt:

+ +{foreach from=$options item=option} +

{@$option['title']}:{if !$option['isMessage']} {@$option['value']}{else} +{@$option['value']|newlineToBreak}{/if}

+{/foreach}]]>
+ + +
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 1ee961f75e..e36da42cc9 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -2273,10 +2273,38 @@ Errors are: + + + + Hello, + +

„{$name}“ sent you a message on {PAGE_TITLE|language} via the contact form:

+ +{foreach from=$options item=option} +

{@$option['title']}:{if !$option['isMessage']} {@$option['value']}{else} +{@$option['value']|newlineToBreak}{/if}

+{/foreach}]]>
+ + + +
+ + + + + + -- 2.20.1