Jump to content

Search the Community

Showing results for 'WireMail'.

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • Welcome to ProcessWire
    • News & Announcements
    • Showcase
    • Wishlist & Roadmap
  • Community Support
    • Getting Started
    • Tutorials
    • FAQs
    • General Support
    • API & Templates
    • Modules/Plugins
    • Themes and Profiles
    • Multi-Language Support
    • Security
    • Jobs
  • Off Topic
    • Pub
    • Dev Talk

Product Groups

  • Form Builder
  • ProFields
  • ProCache
  • ProMailer
  • Login Register Pro
  • ProDrafts
  • ListerPro
  • ProDevTools
  • Likes
  • Custom Development

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


AIM


MSN


Website URL


ICQ


Yahoo


Jabber


Skype


Location


Interests

  1. Wire Mail SMTP An extension to the (new) WireMail base class that uses SMTP-transport This module integrates EmailMessage, SMTP and SASL php-libraries from Manuel Lemos into ProcessWire. I use this continously evolved libraries for about 10 years now and there was never a reason or occasion not to do so. I use it nearly every day in my office for automated composing and sending personalized messages with attachments, requests for Disposition Notifications, etc. Also I have used it for sending personalized Bulkmails many times. The WireMailSmtp module extends the new email-related WireMail base class introduced in ProcessWire 2.4.1 (while this writing, the dev-branch only). Here are Ryans announcement. Current Version 0.8.0 (from 2024-09-25 -- initial version 0.0.1 was pushed on 2014-03-01) Changelog: https://github.com/horst-n/WireMailSmtp/blob/master/CHANGELOG.md Downlod: get it from the Modules Directory || fetch it from Github || or use the module-installer in PWs admin site modules panel with its class name "WireMailSmtp". Install and Configure Download the module into your site/modules/ directory and install it. In the config page you fill in settings for the SMTP server and optionaly the (default) sender, like email address, name and signature. You can test the smtp settings directly there. If it says "SUCCESS! SMTP settings appear to work correctly." you are ready to start using it in templates, modules or bootstrap scripts. Usage Examples The simplest way to use it: $numSent = wireMail($to, $from, $subject, $textBody); $numSent = wireMail($to, '', $subject, $textBody); // or with a default sender emailaddress on config page This will send a plain text message to each recipient. You may also use the object oriented style: $mail = wireMail(); // calling an empty wireMail() returns a wireMail object $mail->to($toEmail, $toName); $mail->from = $yourEmailaddress; // if you don't have set a default sender in config // or if you want to override that $mail->subject($subject); $mail->body($textBody); $numSent = $mail->send(); Or chained, like everywhere in ProcessWire: $mail = wireMail(); $numSent = $mail->to($toEmail)->subject($subject)->body($textBody)->send(); Additionaly to the basics there are more options available with WireMailSmtp. The main difference compared to the WireMail BaseClass is the sendSingle option. With it you can set only one To-Recipient but additional CC-Recipients. $mail = wireMail(); $mail->sendSingle(true)->to($toEmail, $toName)->cc(array('person1@example.com', 'person2@example.com', 'person3@example.com')); $numSent = $mail->subject($subject)->body($textBody)->send(); The same as function call with options array: $options = array( 'sendSingle' => true, 'cc' => array('person1@example.com', 'person2@example.com', 'person3@example.com') ); $numSent = wireMail($to, '', $subject, $textBody, $options); There are methods to your disposal to check if you have the right WireMail-Class and if the SMTP-settings are working: $mail = wireMail(); if($mail->className != 'WireMailSmtp') { // Uups, wrong WireMail-Class: do something to inform the user and quit echo "<p>Couldn't get the right WireMail-Module (WireMailSmtp). found: {$mail->className}</p>"; return; } if(!$mail->testConnection()) { // Connection not working: echo "<p>Couldn't connect to the SMTP server. Please check the {$mail->className} modules config settings!</p>"; return; } A MORE ADVANCED DEBUG METHOD! You can add some debug code into a template file and call a page with it: $to = array('me@example.com'); $subject = 'Wiremail-SMTP Test ' . date('H:i:s') . ' äöü ÄÖÜ ß'; $mail = wireMail(); if($mail->className != 'WireMailSmtp') { echo "<p>Couldn't get the right WireMail-Module (WireMailSmtp). found: {$mail->className}</p>"; } else { $mail->from = '--INSERT YOUR SENDER ADDRESS HERE --'; // <--- !!!! $mail->to($to); $mail->subject($subject); $mail->sendSingle(true); $mail->body("Titel\n\ntext text TEXT text text\n"); $mail->bodyHTML("<h1>Titel</h1><p>text text <strong>TEXT</strong> text text</p>"); $dump = $mail->debugSend(1); } So, in short, instead of using $mail->send(), use $mail->debugSend(1) to get output on a frontend testpage. The output is PRE formatted and contains the areas: SETTINGS, RESULT, ERRORS and a complete debuglog of the server connection, like this one: Following are a ... List of all options and features testConnection () - returns true on success, false on failures sendSingle ( true | false ) - default is false sendBulk ( true | false ) - default is false, Set this to true if you have lots of recipients (50+) to ($recipients) - one emailaddress or array with multiple emailaddresses cc ($recipients) - only available with mode sendSingle, one emailaddress or array with multiple emailaddresses bcc ($recipients) - one emailaddress or array with multiple emailaddresses from = 'person@example.com' - emailaddress, can be set in module config (called Sender Emailaddress) but it can be overwritten here fromName = 'Name Surname' - optional, can be set in module config (called Sender Name) but it can be overwritten here priority (3) - 1 = Highest | 2 = High | 3 = Normal | 4 = Low | 5 = Lowest dispositionNotification () or notification () - request a Disposition Notification subject ($subject) - subject of the message body ($textBody) - use this one alone to create and send plainText emailmessages bodyHTML ($htmlBody) - use this to create a Multipart Alternative Emailmessage (containing a HTML-Part and a Plaintext-Part as fallback) addSignature ( true | false ) - the default-behave is selectable in config screen, this can be overridden here (only available if a signature is defined in the config screen) attachment ($filename, $alternativeBasename = "") - add attachment file, optionally alternative basename send () - send the message(s) and return number of successful sent messages debugSend(1) - returns and / or outputs a (pre formatted) dump that contains the areas: SETTINGS, RESULT, ERRORS and a complete debuglog of the server connection. (See above the example code under ADVANCED DEBUG METHOD for further instructions!) getResult () - returns a dump (array) with all recipients (to, cc, bcc) and settings you have selected with the message, the message subject and body, and lists of successfull addresses and failed addresses, logActivity ($logmessage) - you may log success if you want logError ($logmessage) - you may log warnings, too. - Errors are logged automaticaly useSentLog (true | false) - intended for usage with e.g. third party newsletter modules - tells the send() method to make usage of the sentLog-methods - the following three sentLog methods are hookable, e.g. if you don't want log into files you may provide your own storage, or add additional functionality here sentLogReset () - starts a new LogSession - Best usage would be interactively once when setting up a new Newsletter sentLogGet () - is called automaticly within the send() method - returns an array containing all previously used emailaddresses sentLogAdd ($emailaddress) - is called automaticly within the send() method Changelog: https://github.com/horst-n/WireMailSmtp/blob/master/CHANGELOG.md
  2. WireMailRouter was set to use WireMailSmtp as primary method, but (if a from and to is specified in verbose settings) WireMailSmtp ignores the test attempt if it is not the WireMail class name: $mail = wireMail(); if($mail->className != 'WireMailSmtp') { $dump = "<p>Couldn't get the right WireMail-Module (WireMailSmtp). found: {$mail->className}</p>"; } else { So basically the full test will not work if WireMailRouter is installed - only the plain connection test. My suggestions are: The config screen should make it clear that verbose debug settings will not work if WireMailSmtp is not the WireMail class and need to be empty to test the connection only, or that the test should temporarily set the WireMail class to be WireMailSmtp - something like: if($from && $to) { // do a verbose debugging if(!$subject) $subject = 'Debug Testmail'; if(!$body) $body = 'Debug Testmail, äöüß'; $mail = wireMail(); $dump = ''; if($mail->className != 'WireMailSmtp') { $wireMailClass = $mail->className; $this->config->wireMail('module', 'WireMailSmtp'); $mail = wireMail(); $dump .= "<p>Currently installed WireMail class is $wireMailClass. Testing with WireMailSmtp, then reverting to $wireMailClass.</p>"; } $mail->from = $from; $mail->to($to); $mail->subject($subject); $mail->sendSingle(true); $mail->body($body); $dump .= $mail->debugSend(3); if(isset($wireMailClass)) $this->config->wireMail('module', $wireMailClass); } else { // only try a testconnection Personally I'm more inclined to (2), but does anyone else have a view?
  3. Ok, I understand. Pico CSS sounds interesting. I have never heard about that. So I have added the Aria attributes to the code, but you have to replace the following file: FormElements/Form.php <?php declare(strict_types=1); namespace FrontendForms; /* * Class for creating the form element * * Created by Jürgen K. * https://github.com/juergenweb * File name: Form.php * Created: 03.07.2022 */ use DateTime; use DOMDocument; use DOMException; use Exception; use ProcessWire\Field as Field; use ProcessWire\HookEvent; use ProcessWire\Language; use ProcessWire\Module; use ProcessWire\User; use ProcessWire\Page; use ProcessWire\Wire; use ProcessWire\WireArray; use ProcessWire\WireData; use ProcessWire\WireException; use ProcessWire\WireMail; use Valitron\Validator; use function ProcessWire\wire as wire; use function ProcessWire\wirePopulateStringTags; use function ProcessWire\_n; use function ProcessWire\wireClassNamespace; use function ProcessWire\wireMail; class Form extends CustomRules { /* constants */ const FORMMETHODS = ['get', 'post']; // array that holds allowed action methods (get, post) /* properties */ protected array $storedFiles = []; // array that holds all files (including overwritten filenames) protected string $doubleSubmission = ''; // value hold by the double form submission session protected string $defaultRequiredTextPosition = 'top'; // the default required text position protected string $doNotReply = ''; // Text for do not reply to automatically generated emails protected array $formElements = []; //array that contains all elements of a form element as objects protected array $formErrors = []; // holds the array containing all form errors after submission protected array $values = []; // array of all form values (key = name of the inputfield) protected bool $showForm = true; // show the form on the page protected string $visitorIP = ''; // the IP of the visitor who is visiting this page protected string $captchaCategory = ''; // the category of the captcha (text or image) protected string $langAppendix = ''; // string, which will be appended to multi-lang config fields inside the db protected string|int $useDoubleFormSubmissionCheck = 1; // Enable checking of multiple submissions protected string|int|bool $useCSRFProtection = 1; // Enable/disable CSRF-Protection protected string $general_desc_position = 'afterInput'; // The position of the input field description -> beforeLabel, afterLabel or afterInput protected string|int|bool $useAriaAttributes = false; // use accessibility attributes // Mail properties - only needed if FrontendForms will be used to send emails protected array $mailPlaceholder = []; // associative array for usage in emails (['placeholdername' => 'text',...]) protected string $defaultDateFormat = 'Y-m-d'; // the default format for date strings protected string $defaultTimeFormat = 'H:i a'; // the default format for time strings protected string $receiverAddress = ''; // the email address of the receiver of the mails protected string $mail_subject = ''; // the subject for a mail sent after form validation protected string $emailTemplatesDirPath = ''; // the path to the email templates directory protected string $emailCustomTemplatesDirPath = ''; // the path to the email custom templates directory protected string $emailTemplate = ''; // the filename of the email template including the extension (fe. template.html) protected string $emailTemplatePath = ''; // the path to the body template protected string $emailCustomTemplatePath = ''; // the path to the custom body template protected array $uploaded_files = []; // array which holds all currently uploaded files with the path as value protected int|null $mail_language_id = null; // property for setting the language for mail templates manually protected int|null $site_language_id = null; // internal property containing the current site language protected string|int|null|bool $submitAjax = 0; // whether to submit the form via Ajax (1) or not (0) protected string|null $ajaxRedirect = null; // redirect to this URL after a valid form has been submitted - only for Ajax submission protected string $redirectURL = ''; // the URL for the redirect after successful for form validation protected string $validated = '0'; // the form is validated (1) or not (0) protected string|null|int|bool $showProgressbar = true; /* objects */ protected Alert $alert; // alert box protected RequiredTextHint $requiredHint; // hint to inform that all required fields have to be filled out protected Wrapper $formElementsWrapper; // the wrapper object over all form elements protected User $user; // the user, who views the form (the page) protected Language $userLang; // the language object of the user/visitor protected Page $page; // the current page object, where the form is used protected object $captcha; // the captcha object /** * Every form must have an id. You can set it custom via the constructor - otherwise a random ID will be * generated. The id will be taken for further automatic id generation of the input fields * @throws WireException */ public function __construct(string $id) { parent::__construct(); // set the path to the template folder for the email templates $this->emailTemplatesDirPath = $this->wire('config')->paths->siteModules . 'FrontendForms/email_templates/'; // set the path to the custom template folder for the email templates $this->emailCustomTemplatesDirPath = $this->wire('config')->paths->site . 'frontendforms-custom-templates/'; // set the path to the email template from the module config if ($this->frontendforms['input_emailTemplate'] != 'none') { $this->emailTemplate = $this->frontendforms['input_emailTemplate']; // set filename $this->emailTemplatePath = $this->emailTemplatesDirPath . $this->emailTemplate; // set file path $this->emailCustomTemplatePath = $this->emailCustomTemplatesDirPath . $this->emailTemplate; // set file path } // set the current user $this->user = $this->wire('user'); if ($this->wire('languages')) { // set the current site language as language for mails $this->mail_language_id = $this->user->language->id; // set the id of the current site language $this->site_language_id = $this->user->language->id; } // set Ajax form submission according to the configuration settings if (array_key_exists('input_ajaxformsubmission', $this->frontendforms)) { $this->setSubmitWithAjax((int)$this->frontendforms['input_ajaxformsubmission']); } // set show/hide progressbar during Ajax form submission according to the configuration settings if (array_key_exists('input_hideProgressBar', $this->frontendforms)) { $this->showProgressbar = !(($this->frontendforms['input_hideProgressBar'] == '1')); } $this->showProgressbar($this->showProgressbar); // set the current page $this->page = $this->wire('page'); // check if LanguageSupport module is installed and multi-language is enabled if ($this->wire('modules')->isInstalled('LanguageSupport') && isset($this->wire('user')->language)) { $this->userLang = $this->user->language; // the language object } $this->setLangAppendix(); // set the appendix for multi-language module configuration fields (fe. __1012) // instantiate all objects first $this->alert = new Alert(); $this->requiredHint = new RequiredTextHint(); $this->formElementsWrapper = new Wrapper(); // set default properties $this->visitorIP = $this->wire('session')->getIP(); $this->showForm = $this->allowFormViewByIP(); // show or hide the form depending on the IP ban $this->setAttribute('method', 'post'); // default is post $this->setAttribute('action', $this->page->url); // stay on the same page - needs to run after the API is ready $this->setAttribute('id', $id); // set the id $this->setAttribute('name', $this->getID() . '-' . time()); $this->setHtml5Validation($this->frontendforms['input_html5_validation']); $this->setAttribute('autocomplete', 'off'); // set autocomplete off by default $this->setTag('form'); // set the form tag $this->setCSSClass('formClass'); // add the CSS class $this->setSuccessMsg($this->getLangValueOfConfigField('input_alertSuccessText')); $this->setErrorMsg($this->getLangValueOfConfigField('input_alertErrorText')); $this->setRequiredTextPosition($this->frontendforms['input_requiredHintPosition']); // set the position for the required text $this->getFormElementsWrapper()->setAttribute('id', $this->getAttribute('id') . '-formelementswrapper'); // add id $this->getFormElementsWrapper()->setAttribute('class', $this->frontendforms['input_wrapperFormElementsCSSClass']); // add css class to the wrapper element $this->useDoubleFormSubmissionCheck($this->useDoubleFormSubmissionCheck); $this->setRequiredText($this->getLangValueOfConfigField('input_requiredText')); $this->logFailedAttempts($this->frontendforms['input_logFailedAttempts']); // enable or disable the logging of blocked visitor's IP depending on config settings $this->setMaxAttempts($this->frontendforms['input_maxAttempts']); // set max attempts $this->setMinTime($this->frontendforms['input_minTime']); // set min time $this->setMaxTime($this->frontendforms['input_maxTime']); // set max time $this->setCaptchaType($this->frontendforms['input_captchaType']); // enable or disable the captcha and set type of captcha // set the folder of the page in assets/files as default target folder for file uploads $this->setUploadPath($this->wire('config')->paths->assets . 'files/' . $this->page->id . '/'); // Global text for auto-generated emails $this->doNotReply = $this->_('This email was generated automatically. So please do not reply to this email.'); // create and set all general placeholder variables $this->createGeneralPlaceholders(); // set global description position according to the module configuration if (array_key_exists('input_descPosition', $this->frontendforms)) { $this->general_desc_position = $this->frontendforms['input_descPosition']; } // add a hook method to render mail templates before sending the mail $this->addHookBefore('WireMail::send', $this, 'renderTemplate'); // add a hook method after sending the mail to remove the session variable "templateloaded" $this->addHookAfter('WireMail::send', $this, 'removeTemplateSession'); } /** * Enable or disable the usage of ARIA attributes on form elements * Can be true/empty or false * If set to true then ARIA attributes will be added to input tags * @param bool $use * @return $this */ public function useAriaAttributes(bool $use = true): self { $this->useAriaAttributes = $use; return $this; } /** * Set the description position on per form base * @param string $pos * @return $this */ public function setDescPosition(string $pos): self { if (in_array($pos, ['beforeLabel', 'afterLabel', 'afterInput'])) { $this->general_desc_position = $pos; // set new position property } return $this; } /** * Create a new mail instance of a given custom mail module if set * Otherwise a new WireMail object will be instantiated * This method is only for other modules based on FrontendForms * @param string|null $class * @return \ProcessWire\WireMail|\FrontendForms\WireMailPostmark|\FrontendForms\WireMailPostmarkApp * @throws \ProcessWire\WireException */ //protected function newMailInstance(string|null $class = null): WireMail|WireMailPostmark|WireMailPostmarkApp|WireMailSmtp|WireMailPHPMailer protected function newMailInstance(string|null $class = null) { // if $class is null, set WireMail() object by default if (is_null($class)) return new WireMail(); // just to play safe - check if the given module is installed first if (!$this->wire('modules')->getModuleID($class)) return new WireMail(); // create a new instance of the given module switch ($class) { case('WireMailPostmark'): case('WireMailPostmarkApp'): return $this->wire('mail')->new(); break; case('WireMailSmtp'): return wireMail(); case('WireMailPHPMailer'): return wire("modules")->get("WireMailPHPMailer"); default: return new WireMail(); } } /** * Get all files that were uploaded */ public function getUploadedFiles(): array { return $this->uploaded_files; } /** * Enable/disable HTML5 form validation * @param bool $validation * @return $this */ public function setHtml5Validation(string|int|bool|null $validation): self { $validation = (bool)$validation; $this->frontendforms['input_html5_validation'] = $validation; if ($validation) { $this->removeAttribute('novalidate'); } else { $this->setAttribute('novalidate'); } return $this; } /** * Return if HTML5 form validation is enabled or not * @return bool */ public function getHTML5Validation(): bool { return $this->frontendforms['input_html5_validation']; } /** * THIS METHOD IS DEPRECATED - USE useAjax() METHOD INSTEAD * Enable/disable form submission via ajax * @param bool|int|null $ajax - true => form will be submitted via Ajax * @return $this */ public function setSubmitWithAjax(bool|int|null|string $ajax = true): self { $this->submitAjax = boolval($ajax); return $this; } /** * Submit a form via AJAX * @param bool|int|string|null $ajax * @return self */ public function useAjax(bool|int|null|string $ajax = true): self { $this->setSubmitWithAjax($ajax); return $this; } /** * Whether to show the progressbar during the Ajax form submission or not * @param bool|int|string|null $showProgressbar * @return $this */ public function showProgressbar(bool|int|null|string $showProgressbar = true): self { $this->showProgressbar = boolval($showProgressbar); return $this; } /** * Get the setting value if Ajax should be used to submit the form * @return bool */ public function getSubmitWithAjax(): bool { return (bool)$this->submitAjax; } /** * Enable/disable checking of double form submissions * True: enabled * False: disabled * @param bool $useDoubleFormSubmissionCheck * @return void * @throws WireException */ public function useDoubleFormSubmissionCheck(int|string|bool $useDoubleFormSubmissionCheck): void { $useDoubleFormSubmissionCheck = $this->sanitizeValueToInt($useDoubleFormSubmissionCheck); // sanitize to int $this->useDoubleFormSubmissionCheck = $useDoubleFormSubmissionCheck; // set the property if ($useDoubleFormSubmissionCheck) { // check if session exists if ($this->wire('session')->get('doubleSubmission-' . $this->getID())) { $this->doubleSubmission = $this->wire('session')->get('doubleSubmission-' . $this->getID()); } else { $this->doubleSubmission = uniqid(); $this->wire('session')->set('doubleSubmission-' . $this->getID(), $this->doubleSubmission); } } else { // remove the session if present $this->wire('session')->remove('doubleSubmission-' . $this->getID()); } } /** * Enable/Disable CSRF-protection check * @param int|string|bool $csrf * @return void */ public function useCSRFProtection(int|string|bool $csrf): void { $this->useCSRFProtection = $this->sanitizeValueToInt($csrf); } public function getCSRFProtection(): bool { return (bool)$this->useCSRFProtection; } /** * Method to disable some methods if form is used inside an iframe on a different domain (crossdomain) * @return void * @throws \ProcessWire\WireException */ public function useFormInCrossDomainIframe(): void { $this->useDoubleFormSubmissionCheck(false); // disable double submission check $this->useCSRFProtection(false); // disable CSRF-Attack check // disable the CAPTCHA because it does not work in crossdomain iframes $this->disableCaptcha(); } /** * Should the form be displayed after a successful submission (true or 1) or not (false or 0) * By default, only the success-message will be displayed after valid form submission and not the whole form * This prevents double form submissions * @param bool|int $show * @return void */ public function showForm(bool|int $show): void { $this->showForm = $show; } /** * Get the value whether the form should be displayed after successful submission or not * @return bool */ public function getShowForm(): bool { return $this->showForm; } /** * Method, that holds an array with all general placeholders * These placeholders can be used in mail templates or mail body templates/texts * The array contains the placeholder name as the key and its value (placeholder name => value) * @return array * @throws WireException */ public function generalPlaceholders(): array { return [ 'domainlabel' => $this->_('Domain'), 'domainvalue' => $this->wire('config')->urls->httpRoot, 'currenturllabel' => $this->_('Visited page'), 'currenturlvalue' => $this->wire('input')->httpHostUrl(), 'iplabel' => $this->_('IP'), 'ipvalue' => $this->wire('session')->getIP(), 'currentdatetimelabel' => $this->_('Date/time'), 'currentdatetimevalue' => $this->getDateTime(), 'currenttimelabel' => $this->_('Time'), 'currenttimevalue' => $this->getTime(), 'currentdatelabel' => $this->_('Date'), 'currentdatevalue' => $this->getDate(), 'usernamelabel' => $this->_('Username'), 'usernamevalue' => $this->user->name, 'browserlabel' => $this->_('Browser'), 'browservalue' => $_SERVER['HTTP_USER_AGENT'], 'donotreplayvalue' => $this->_('This is an auto generated message, please do not reply.') ]; } /** * Create the default progress bar * @return string */ public function createProgressbar(): string { return ' <div class="cssProgress"> <div class="progress1"> <div class="cssProgress-bar cssProgress-active cssProgress-success" data-percent="100" style="width: 100%; transition: none 0s ease 0s;"> </div> </div> </div> '; } /** * Method to add all general placeholders als name => value pair to the placeholder array * @return void * @throws WireException */ protected function createGeneralPlaceholders(): void { foreach ($this->generalPlaceholders() as $placeholderName => $placeholderValue) { $this->setMailPlaceholder($placeholderName, $placeholderValue); } } /** * Set the appendix for usage in multi-language configuration fields * fe if user has default language, the appendix is an empty string * if the user has another language chosen, than the appendix consists of 2 underscores and the lang id (__1012) * @return void * @throws WireException */ protected function setLangAppendix(): void { if ($this->wire('languages')) { $this->langAppendix = $this->userLang->isDefault() ? '' : '__' . $this->userLang->id; } } /** * Special general methods for sending emails */ public static function checkForPath(string $pathfilename): bool { $pathInfo = pathinfo($pathfilename); if ($pathInfo['dirname'] !== '.') return true; return false; } /** * Include the template in the mail if it was set in the configuration or directly on the WireMail object * Takes the input_emailTemplate property to check whether a template should be used or not * @param Module|wire|WireArray|WireData $mail * @return void * @throws WireException * @throws DOMException * @throws Exception */ protected function includeMailTemplate(Module|Wire|WireArray|WireData $mail): void { // set email_template property if it was not set before if (!$mail->email_template) { $mail->email_template = $this->frontendforms['input_emailTemplate']; } // check if email template is set if ($mail->email_template != 'none') { // set body as placeholder if ($mail->email_template == 'inherit') { // use the value from the FrontendForms module configuration $mail->email_template = $this->frontendforms['input_emailTemplate']; } if ($mail->email_template != 'none') { // check if template name or template path has been added if (self::checkForPath($mail->email_template)) { $this->emailTemplatesDirPath = $this->emailCustomTemplatesDirPath = ''; } if ($this->wire('files')->exists($this->emailTemplatesDirPath . $mail->email_template)) { $body = $this->loadTemplate($this->emailTemplatesDirPath . $mail->email_template); } else if ($this->wire('files')->exists($this->emailCustomTemplatesDirPath . $mail->email_template)) { $body = $this->loadTemplate($this->emailCustomTemplatesDirPath . $mail->email_template); } else { throw new Exception(sprintf('Mail could not be sent, because the mail template with the name %s does not exist.', $mail->email_template)); } // add pre-header text (if present) right after the opening body tag if ($mail->title) { $doc = new DOMDocument(); $doc->loadHTML($body); $bodyTags = $doc->getElementsByTagName('body'); if ($bodyTags->length > 0) { $bodyElement = $bodyTags->item(0); $preheader = $doc->createElement('div', $mail->title . $this->getLitmusHack()); $preheader->setAttribute('style', $this->getPreheaderStyle()); $bodyElement->insertBefore($preheader, $bodyElement->firstChild); $body = $doc->saveHTML(); } } // if bodyHTML is set, set a body placeholder by default out of the content switch ($mail->className()) { case('WireMailPHPMailer'): $this->setMailPlaceholder('body', $mail->Body); break; default: // default WireMail class used $this->setMailPlaceholder('body', $mail->bodyHTML); } // render [[BODY]] placeholder if it is present and convert all placeholders inside it if ($this->getMailPlaceholder('body')) { $bodyPlaceholder = $this->getMailPlaceholder('body'); $bodyPlaceholder = wirePopulateStringTags($bodyPlaceholder, $this->getMailPlaceholders(), ['tagOpen' => '[[', 'tagClose' => ']]']); $this->setMailPlaceholder('body', $bodyPlaceholder); } $body = wirePopulateStringTags($body, $this->getMailPlaceholders(), ['tagOpen' => '[[', 'tagClose' => ']]']); // set the result as the bodyHTML of the email // if bodyHTML is set, set a body placeholder by default out of the content switch ($mail->className()) { case('WireMailPHPMailer'): $mail->Body = $body; break; default: // default WireMail class used $mail->bodyHTML($body); } } } else { // add invisible div with email pre-header to the top of the email body switch ($mail->className()) { case('WireMailPHPMailer'): $mail->Body = $this->generateEmailPreHeader($mail) . $mail->Body; break; default: // default WireMail class used $mail->bodyHTML($this->generateEmailPreHeader($mail) . $mail->bodyHTML); } } } /** * Check if the form has at least one file upload field * Needs to be called after all fields were added * @return bool -> true: a file upload field was found, false: no file upload field found */ protected function hasFileUploadField(): bool { if (($this->hasAttribute('enctype')) && ($this->getAttribute('enctype') == 'multipart/form-data')) { return true; } return false; } /** * If file upload fields are present in a form - get an array of objects containing all file upload fields * @return array */ protected function getFileUploadFields(): array { $fields = []; if ($this->hasFileUploadField()) { foreach ($this->formElements as $uploadfield) { if (($uploadfield instanceof InputFile) || (is_subclass_of($uploadfield, 'InputFile'))) { $fields[] = $uploadfield; } } } return $fields; } /** * Render the mail template: replace placeholders and use HTML email template if set * @param \ProcessWire\HookEvent $event * @return \ProcessWire\Module|\ProcessWire\Wire|\ProcessWire\WireArray|\ProcessWire\WireData * @throws \DOMException * @throws \ProcessWire\WireException * @throws \ProcessWire\WirePermissionException */ public function renderTemplate(HookEvent $event): Module|Wire|WireArray|WireData { $mail = $event->object; // do not add a template if template is set to "none" if ($mail->email_template !== 'none') { // set the placeholder for the title if present $this->setMailPlaceholder('title', $mail->title); // set the placeholder for the body if (($mail->bodyHTML) || ($mail->bodyHtml) || ($mail->body)) { // set HTML as preferred value if ($mail->bodyHTML) { $content = $mail->bodyHTML; } else if ($mail->bodyHtml) { $content = $mail->bodyHtml; } else { $content = $mail->body; } $body = wirePopulateStringTags($content, $this->getMailPlaceholders(), ['tagOpen' => '[[', 'tagClose' => ']]']); $this->setMailPlaceholder('body', $body); $mail->bodyHTML($body); $mail->body($body); } if ($this->wire('session')->get('templateloaded') != '1') { $this->includeMailTemplate($mail); // include/use mail template if set $this->wire('session')->set('templateloaded', '1'); } } else { // populate Placeholders even if no template is used to send emails switch ($mail->className()) { case('WireMailPHPMailer'): $mail->Body = wirePopulateStringTags($mail->bodyHTML, $this->getMailPlaceholders(), ['tagOpen' => '[[', 'tagClose' => ']]']); break; default: // default WireMail class used $mail->bodyHTML(wirePopulateStringTags($mail->bodyHTML, $this->getMailPlaceholders(), ['tagOpen' => '[[', 'tagClose' => ']]'])); } } return $mail; } /** * Set the mail body property depending on the custom mail module set * @param $mail * @param string|null $body * @return void */ public static function setBody($mail, string|null $body, string $mailModule): void { if (is_null($body)) $body = ''; // add support for WireMailPHPMailer - has a different name for the bodyHTML property if ($mailModule === 'WireMailPHPMailer') { $mail->Body = $body; } else { $mail->bodyHTML($body); } } /** * This method prevents the multiple embedding of the email template if there are multiple forms on one page. * @return void * @throws \ProcessWire\WireException */ public function removeTemplateSession(): void { $this->wire('session')->remove('templateloaded'); } /** * Load a template file from the given path including php code and output it as a string * @param string $templatePath - the path to the template that should be rendered * @return string - the html template */ protected function loadTemplate(string $templatePath): string { ob_start(); include($templatePath); $var = ob_get_contents(); ob_end_clean(); return $var; } /** * Set the recipient email address on per-form base * In this case, the recipient can be set/changed on per-form base instead of directly on the WireMail object * Needed in every case, where the WireMail object is not directly reachable * @param string $email * @return $this * @throws WireException * @throws Exception */ public function to(string $email): self { if ($this->wire('sanitizer')->email($email)) { $this->receiverAddress = $email; } else { throw new Exception("Email address for the recipient is not a valid email address.", 1); } return $this; } /** * Set the subject for the email on per-form base * In this case, the subject can be set/changed on per-form base instead of directly on the WireMail object * Needed in every case, where the WireMail object is not directly reachable * @param string $subject * @return $this */ public function subject(string $subject): self { $this->mail_subject = $subject; return $this; } /** * Get a date string in the given format as set in the config of the module * If no value is entered as parameter, the current date will be displayed * @param string|null $dateTime * @return string * @throws WireException */ public function getDate(string|null $dateTime = null): string { $dateTime = (is_null($dateTime)) ? time() : $dateTime; // get user language if ($this->wire('languages')) { $langID = '__' . $this->user->language->id; } else { $langID = ''; } $fieldName = 'input_dateformat' . $langID; $format = $this->frontendforms[$fieldName] ?? $this->defaultDateFormat; return $this->wire('datetime')->date($format, $dateTime); } /** * Get a time string in the given format as set in the config of the module * If no value is entered as parameter, the current time will be displayed * @param string|null $dateTime * @return string * @throws WireException */ public function getTime(string|null $dateTime = null): string { $dateTime = (is_null($dateTime)) ? time() : $dateTime; // get user language if ($this->wire('languages')) { $langID = '__' . $this->user->language->id; } else { $langID = ''; } $fieldName = 'input_timeformat' . $langID; $format = $this->frontendforms[$fieldName] ?? $this->defaultTimeFormat; return $this->wire('datetime')->date($format, $dateTime); } /** * Get a combined date and time string in the given format as set in the config of the module * If no value is entered as parameter, the current date and time will be displayed * @param string|null $dateTime * @return string * @throws WireException */ public function getDateTime(string|null $dateTime = null): string { return $this->getDate($dateTime) . ' ' . $this->getTime($dateTime); } /** * Set a new placeholder variable with a specific value to the mailPlaceholder array * @param string $placeholderName * @param string|array|null $placeholderValue * @return $this */ public function setMailPlaceholder(string $placeholderName, string|array|null $placeholderValue): self { if (!is_null($placeholderValue)) { $placeholderName = strtoupper(trim($placeholderName)); if (is_array($placeholderValue)) { // check if array is multidimensional like multiple file uploads if (count($placeholderValue) == count($placeholderValue, COUNT_RECURSIVE)) { // one-dimensional: convert array of values to comma separated string $placeholderValue = implode(', ', $placeholderValue); } else { $file_names = []; // multi-dimensional $_FILES array foreach ($placeholderValue as $file) { // adding all file names to the array - independent if the name exists or not $file_names[] = $file['name']; } // clean the array by removing empty array elements $placeholderValue = implode(',', array_filter($file_names)); } } // trim and merge it to the mailPlaceholder array $this->mailPlaceholder = array_merge($this->getMailPlaceholders(), [$placeholderName => trim($placeholderValue)]); } return $this; } /** * Remove a placeholder by its name from the placeholder array if it is present * @param string $placeholderName * @return void */ public function removePlaceholder(string $placeholderName): void { $key = strtoupper(trim($placeholderName)); if (array_key_exists($key, $this->getMailPlaceholders())) { unset($this->getMailPlaceholders()[$key]); } } /** * Get all placeholder variables and their values * For usage in body template of emails * @return array */ public function getMailPlaceholders(): array { return $this->mailPlaceholder; } /** * Get the value of a certain placeholder by its name * @param string $placeholderName * @return string */ public function getMailPlaceholder(string $placeholderName): string { $content = ''; $placeholderName = strtoupper($placeholderName); if (array_key_exists($placeholderName, $this->mailPlaceholder)) { $content = $this->mailPlaceholder[$placeholderName]; } return $content; } /** * Get all included classes of the form fields * For usage in body template of emails * @return array */ protected function getFormFieldClasses(): array { $classes = []; foreach ($this->formElements as $fieldObject) { $classes[] = $fieldObject->className(); } return $classes; } /** * Check if an input field with a specific name is present the current form (but not if it has a value) * @param string $fieldName * @return bool */ public function formfieldExists(string $fieldName): bool { $fieldName = (trim($fieldName)); return (in_array(strtolower($fieldName), array_map("strtolower", $this->getFormFieldClasses()))); } /** * Output the value of multilang fields from the module configuration * @param string $fieldName * @param array|null $modulConfig * @param int|null $lang_id * @return string */ protected function getLangValueOfConfigField( string $fieldName, array $modulConfig = null, int|null $lang_id = null ): string { $modulConfig = (is_null($modulConfig)) ? $this->frontendforms : $modulConfig; $langAppendix = (is_null($lang_id)) ? $this->langAppendix : '__' . $lang_id; $fieldNameLang = $fieldName . $langAppendix; if (isset($modulConfig[$fieldNameLang])) { return $modulConfig[$fieldNameLang] != '' ? $modulConfig[$fieldNameLang] : $modulConfig[$fieldName]; } return $modulConfig[$fieldName]; } /** * Method to sanitize string, integer or boolean value to integer value 1 and 0 * This is necessary, because configuration values of checkboxes are stored as integers in the db * @param string|int|bool $value * @return int */ protected function sanitizeValueToInt(string|int|bool $value): int { if (is_string($value)) { if ($value !== '') { return 1; } return 0; } else { if (is_int($value)) { if ($value >= 1) { return 1; } return 0; } else { return (int)$value; } } } /** * Set a custom upload path for uploaded files * If no path is selected, then the files will be stored inside the dir of this page in site/assets/files * @param string $path_to_folder * @return Form */ public function setUploadPath(string $path_to_folder): self { $this->uploadPath = trim($path_to_folder); return $this; } /** * This method is only for testing of ip addresses that should be banned * Enter ip addresses as a numeric array * @param string $ip * @return void * @throws Exception */ public function testIPBan(string $ip): void { if (filter_var($ip, FILTER_VALIDATE_IP)) { $this->visitorIP = $ip; } else { throw new Exception(sprintf($this->_('%s is not a valid IP address.'), $ip)); } } /** * Disable/enable the IP ban on per form base * @param bool $enabled * @return void */ public function useIPBan(int|string|bool $enabled): void { $this->frontendforms['input_useIPBan'] = $this->sanitizeValueToInt($enabled); } /** * Disable/enable and set type of captcha on per-form base * @param string $captchaType * @return void */ protected function setCaptchaType(string $captchaType): void { $this->frontendforms['input_captchaType'] = $captchaType; if ($this->frontendforms['input_captchaType'] !== 'none') { $this->setCaptchaCategory($captchaType); // $this->captcha = AbstractCaptchaFactory::make($this->getCaptchaCategory(), $this->frontendforms['input_captchaType']); } } /** * Public method to disable the captcha on per form base if needed * @return void */ public function disableCaptcha(): void { $this->setCaptchaType('none'); } /** * Get the captcha type set * @return string */ protected function getCaptchaType(): string { return $this->frontendforms['input_captchaType']; } /** * Set the captcha category (text, image) depending on the captcha type * @param string $captchaType * @return void */ protected function setCaptchaCategory(string $captchaType): void { $this->captchaCategory = AbstractCaptchaFactory::getCaptchaTypeFromClass($captchaType); } /** * Get the captcha category * @return string */ public function getCaptchaCategory(): string { return $this->captchaCategory; } /** * Get the captcha object for further manipulations * @return object|null */ protected function getCaptcha(): object|null { return $this->captcha; } /** * Check if the visitor is on the blacklist or not * @return bool - true if the visitor is not on the blacklist */ protected function allowFormViewByIP(): bool { if (!$this->frontendforms['input_useIPBan']) { return true; } if ($this->frontendforms['input_preventIPs'] === '') { return true; } $ipaddresses = $this->newLineToArray($this->frontendforms['input_preventIPs']); return !in_array($this->visitorIP, $ipaddresses); } /** * Convert the values of a textbox to an array * Will be needed for the list of banned IP in the module configuration * @param string|null $textarea - the value of the textarea field * @return array */ protected function newLineToArray(string $textarea = null): array { $final_array = []; if (!is_null($textarea)) { $textarea_array = array_map('trim', explode("\n", $textarea)); // remove extra spaces from each array value foreach ($textarea_array as $textarea_arr) { $final_array[] = trim($textarea_arr); } } return $final_array; } /** * Enable or disable the logging of blocked visitor's IP on per form base * True: logging is enabled * False: logging is disabled * @param string|bool|int $logFailedAttempts * @return void */ public function logFailedAttempts(string|bool|int $logFailedAttempts): void { $this->frontendforms['input_logFailedAttempts'] = $this->sanitizeValueToInt($logFailedAttempts); } /** * Convert post values to a string * @param bool $showButtonValues * @return string */ public function getValuesAsString(bool $showButtonValues = false): string { $postData = $this->flattenMixedArray($this->getValues($showButtonValues)); $dataAttributes = array_map(function ($value, $key) { return $key . '=' . $value; }, array_values($postData), array_keys($postData)); return implode(', ', $dataAttributes); } /** * Returns a numeric array of all available languages * Only for internal usage * * @return array * @throws WireException */ protected function getAllAvailableLanguages(): array { $path = $this->wire('config')->paths->siteModules . 'FrontendForms/lang'; $langFiles = $this->wire('files')->find($path); $languages = []; foreach ($langFiles as $lang) { $languages[] = basename($lang, '.php'); } return $languages; } /** * Enable/disable the wrapping of checkboxes by its label * This is useful for some cases where you need to add the label after the input (fe. some CSS frameworks * @param bool $wrap * @return void */ public function appendLabelOnCheckboxes(bool $wrap): void { $this->appendcheckbox = $wrap; } /** * Get the value of appendcheckbox * @return bool */ protected function getAppendLabelOnCheckboxes(): bool { return $this->appendcheckbox; } /** * Enable/disable the wrapping of radios by its label * This is useful for some cases where you need to add the label after the input (fe. some CSS frameworks * @param bool $wrap * @return void */ public function appendLabelOnRadios(bool $wrap): void { $this->appendradio = $wrap; } /** * Get the value of appendradio * @return bool */ protected function getAppendLabelOnRadios(): bool { return $this->appendradio; } /** * Set your own text for required fields * @param string $requiredText * @return RequiredTextHint */ public function setRequiredText(string $requiredText): RequiredTextHint { if ($requiredText === '') { $requiredText = $this->_('All fields marked with (*) are mandatory and must be completed.'); } $this->requiredHint->setText($requiredText); return $this->requiredHint; } /** * Get the required text hint object for further manipulations * @return RequiredTextHint */ public function getRequiredText(): RequiredTextHint { return $this->requiredHint; } /** * This method creates an inner wrapper over all form elements if set to true, or it removes the wrapper if set * to false So it adds a <div> tag after the opening form tag and a </div> tag before the closing form tag * @param bool $useFormElementsWrapper * @return Wrapper */ public function useFormElementsWrapper(int|string|bool $useFormElementsWrapper): Wrapper { $useFormElementsWrapper = $this->sanitizeValueToInt($useFormElementsWrapper); // sanitize to int $this->frontendforms['input_wrapperFormElements'] = $useFormElementsWrapper; return $this->formElementsWrapper; } /** * Return the wrapper object * @return Wrapper */ public function getFormElementsWrapper(): Wrapper { return $this->formElementsWrapper; } /** * Set the success message for successful form submission * Can be used to overwrite the default success message * @param string $successMsg * @return void */ public function setSuccessMsg(string $successMsg): void { if ($successMsg === '') { $successMsg = $this->_('Thank you for your message.'); } $this->frontendforms['input_alertSuccessText'] = trim($successMsg); } /** * Set the error message if errors occur after form submission * Can be used to overwrite the default error message * @param string $errorMsg * @return void */ public function setErrorMsg(string $errorMsg): void { if ($errorMsg === '') { $errorMsg = $this->_('Sorry, some errors occur. Please check your inputs once more.'); } $this->frontendforms['input_alertErrorText'] = trim($errorMsg); } /** * Static method to check if SeoMaestro is installed or not * returns the SeoMaestro object on true, otherwise null * @return Field|null */ public static function getSeoMaestro(): ?Field { if (wire('modules')->isInstalled("SeoMaestro")) { // grab seo maestro input field $seoField = wire('fields')->find('type=FieldtypeSeoMaestro'); if ($seoField) { return $seoField->first(); } } return null; } /** * Get the value of a specific formfield after form submission by its name * Can be used to send fe this value via email to a recipient or store it inside the db * You can enter pure name or name attribute including form prefix * @param string $name - the name attribute of the input field * @return string|array|null */ public function getValue(string $name): string|array|null { $name = $this->createElementName(trim($name)); if ($this->getValues()) { // first check if the name exists if (isset($this->getValues()[$name])) { return $this->getValues()[$name]; } else { if (isset($this->getValues()[$this->getID() . '-' . $name])) { // check if name including form id prefix exists return $this->getValues()[$this->getID() . '-' . $name]; } } return null; } return null; } /** * Add the form id as prefix to the name attribute * @param string $name - the name attribute of the element * @return string - returns the name attribute including the form id as prefix */ private function createElementName(string $name): string { $name = trim($name); $formID = $this->getID(); if (!str_starts_with($name, $formID)) { $name = $formID . '-' . $name; } return $name; } /** * Get all sanitized form values after form submission as an array * If there are sanitizers set for the form values, they will be applied * @param bool $buttonValue * @return array|null */ public function getValues(bool $buttonValue = false): array|null { if ($buttonValue) { return $this->values; } $result = array_intersect($this->getNamesOfInputFields(), $this->getNamesOfInputFields()); $values = []; foreach ($result as $key) { // check if inputfield is a file upload field $formElement = $this->getFormelementByName($key); if ($formElement instanceof InputFile) { $files = []; if ($this->storedFiles) { $pathFileArray = $this->storedFiles; $filesArray = []; foreach ($pathFileArray as $path) { // output only the basename without the whole path $filesArray[] = pathinfo($path, PATHINFO_BASENAME); } $values[$key] = $filesArray; } else { if (is_array($_FILES[$key]['name'])) { // multiple upload field foreach ($_FILES[$key]['name'] as $filename) { $files[] = strtolower($this->wire('sanitizer')->filename($filename)); } $values[$key] = $files; } else { // single upload field $values[$key] = $_FILES[$key]['name']; } } } else { if (array_key_exists($key, $this->values)) { $values[$key] = $this->values[$key]; } } } return $values; // array } /** * Get all Elements (inputs, buttons, ...) that are added to the form object * @return array - returns an array of all form element objects */ public function getFormElements(): array { return $this->formElements; } /** * Get a specific element of the form by entering the name of the element as parameter * With this method you can grab and manipulate a specific element * @param string $name - the name attribute of the element (fe email) * @param boolean $checkPrefix - true to check if form id is added for inputfield name or false to ignore this * @return object|bool - the form element object or false if not found */ public function getFormelementByName(string $name, bool $checkPrefix = true): object|bool { //check if id of the form was added as prefix of the element name if ($checkPrefix) { $name = $this->createElementName($name); } return current(array_filter($this->formElements, function ($e) use ($name) { return $e->getAttribute('name') == $name; })); } /** * Overwrite the global setting for the required text position on per form base * @param string $position - has to be 'top' or 'bottom' * @return void */ public function setRequiredTextPosition(string $position): void { $position = trim($position); $this->defaultRequiredTextPosition = in_array($position, ['none', 'top', 'bottom']) ? $position : 'top'; } /** * Get the alert object for further manipulations * @return Alert */ public function getAlert(): Alert { return $this->alert; } /** * If you want to disable it, add this method to the form object - not recommended * @param int|string|bool $honeypot * @return void */ public function useHoneypot(int|string|bool $honeypot): void { $this->frontendforms['input_useHoneypot'] = $this->sanitizeValueToInt($honeypot); } /** * Method to rearrange the multiple files array $_FILES * @param array $file_post * @return array */ private function reArrayFiles(array $file_post): array { $file_ary = array(); if ($file_post['error'] != 4) { $file_count = count($file_post['name']); $file_keys = array_keys($file_post); for ($i = 0; $i < $file_count; $i++) { foreach ($file_keys as $key) { $file_ary[$i][$key] = $file_post[$key][$i]; } } } return $file_ary; } /** * Internal method to store uploaded files via InputFile field in the chosen folder * @param array $formElements * @return array * @throws WireException */ private function storeUploadedFiles(array $formElements): array { $uploaded_files = []; if ($_FILES) { // create directory if it does not exist $this->wire('files')->mkdir($this->uploadPath); // get all upload fields inside the form foreach ($formElements as $element) { if (($element instanceof InputFile) || (is_subclass_of($element, 'InputFile'))) { $fieldName = $element->getAttribute('name'); // the name of the upload field if ($element->getMultiple()) { // multiple files $files = $this->reArrayFiles($_FILES[$fieldName]); foreach ($files as $file) { if ($file['error'] == 0) { // sanitize file name and convert it to lowercase to prevent problems on certain servers $filename = $this->wire('sanitizer')->filename($file['name'], true); $target_file = $this->uploadPath . strtolower($filename); $uploaded_files[] = $target_file; move_uploaded_file($file['tmp_name'], $target_file); } } } else { // single file $file = $_FILES[$fieldName]; if ($file['error'] == 0) { // sanitize file name and convert it to lowercase to prevent problems on certain servers $filename = $this->wire('sanitizer')->filename(basename($file['name']), true); $target_file = $this->uploadPath . strtolower($filename); $uploaded_files[] = $target_file; move_uploaded_file($file['tmp_name'], $target_file); } } } } } return $uploaded_files; } /** * Convert the complicated $_FILES array to a simpler one * @param array $files * @return array */ protected function simplifyMultiFileArray(array $files = []): array { $sFiles = []; if (is_array($files) && $files['error'] != '4') { foreach ($files as $key => $file) { foreach ($file as $index => $attr) { $sFiles[$index][$key] = $attr; } } } return $sFiles; } /** * Internal method to put required rule always on the first place of validation * Checking if value is present is always logical the first step before checking for other things * @param array $rules * @return array */ protected function putRequiredOnTop(array $rules): array { if (count($rules) > 1) { if (array_key_exists('required', $rules)) { $rules = ['required' => $rules['required']] + $rules; } } return $rules; } /** * Process the form after form submission * Includes sanitization and validation * @return bool - true: form is valid, false: form has errors * @throws WireException * @throws Exception */ public function ___isValid(): bool { // set WireInput array depth to 2 because auf multiple file uploads $this->wire('config')->wireInputArrayDepth = 2; $formMethod = $this->getAttribute('method'); // grab the method (get or post) $input = $this->wire('input')->$formMethod; // get the GET or POST values after submission $formElements = $this->formElements; //grab all form elements as an array of objects // check for file upload fields inside the form $file_upload_fields = $this->getFileUploadFields(); if ($file_upload_fields) { foreach ($file_upload_fields as $field) { $name = $field->getAttribute('name'); if (!empty($_FILES)) { if ($field->hasAttribute('multiple')) { // convert $_FILES array to a simpler one $input[$name] = $this->simplifyMultiFileArray($_FILES[$name]); } else { $input[$name] = $_FILES[$name]; } } } } // instantiates the FormValidation object $validation = new FormValidation($input, $this, $this->alert); // 1) check if this form was submitted and no other form on the same page if ($validation->thisFormSubmitted()) { // 2) check if form was submitted in time range if ($validation->checkTimeDiff($formElements)) { // 3) check if max attempts were reached if ($validation->checkMaxAttempts($this->wire('session')->attempts)) { // 4) check for double form submission if ($validation->checkDoubleFormSubmission($this, $this->useDoubleFormSubmissionCheck)) { // 5) Check for CSRF attack if ($validation->checkCSRFAttack($this->getCSRFProtection())) { /* START PROCESSING THE FORM */ //add honeypotfield to the array because it will be rendered afterwards if ($this->frontendforms['input_useHoneypot']) { $formElements[] = $this->createHoneypot(); } //add captcha to the array because it will be rendered afterwards if ($this->getCaptchaType() !== 'none') { $formElements[] = $this->getCaptcha()->createCaptchaInputField($this->getID()); } // Get only input field for user inputs (no fieldsets, buttons,..) $formElements = $validation->getRealInputFields($formElements); // Run sanitizer on all POST values first $sanitizedValues = []; foreach ($formElements as $element) { // remove all form elements which have the disabled attribute, because they do not send values if (!$element->hasAttribute('disabled')) { if ($element instanceof InputFile) { $file_upload_name = $element->getAttribute('name'); if ($element->getMultiple()) { $sanitizedValues[$file_upload_name] = $this->reArrayFiles($_FILES[$file_upload_name]); } else { $sanitizedValues[$file_upload_name] = [$_FILES[$file_upload_name]]; } } else { $sanitizedValues[$element->getAttribute('name')] = $validation->sanitizePostValue($element); } //$sanitizedValues[$element->getAttribute('name')] = $validation->sanitizePostValue($element); } else { // remove all validation rules from this element $element->removeAllRules(); } } $v = new Validator($sanitizedValues); foreach ($formElements as $element) { // run validation only if there is at least one validation rule set if (count($element->getRules()) > 0) { // add required validation to be the first $rules = $this->putRequiredOnTop($element->getRules()); foreach ($rules as $validatorName => $parameters) { $v->rule($validatorName, $element->getAttribute('name'), ... $parameters['options']); // Add custom error message text if present if (isset($parameters['customMsg'])) { $v->message($parameters['customMsg']); } if (isset($parameters['customFieldName'])) { $v->label($parameters['customFieldName']); } else { if ($element->getLabel()->getText()) { // use the label if present, otherwise use the name attribute $v->label($element->getLabel()->getText()); } } } } // add honeypot validation if honeypot field is included if ($this->frontendforms['input_useHoneypot']) { if ($element->getAttribute('name') == $this->createElementName('seca')) { $v->rule('length', $element->getAttribute('name'), 0)->message($this->_('Please do not fill out this field')); } } // add captcha validation if captcha field is included if ($this->getCaptchaType() !== 'none') { if ($element->getAttribute('name') == $this->createElementName('captcha')) { $v->rule('required', $element->getAttribute('name'))->label($this->_('The captcha')); // captcha is always required $v->rule('checkCaptcha', $element->getAttribute('name'), $this->wire('session')->get('captcha_' . $this->getID()))->label($this->_('The CAPTCHA')); } } $this->setValues(); } if ($v->validate()) { $this->validated = '1'; $this->alert->setCSSClass('alert_successClass'); $this->alert->setText($this->getSuccessMsg()); $this->wire('session')->remove('attempts'); // remove attempt session $this->wire('session')->remove('doubleSubmission-' . $this->getID()); // remove the session for checking for double form submission $this->showForm = false; // check if files were uploaded and store them inside the chosen folder $this->uploaded_files = $this->storeUploadedFiles($formElements); // remove session added by matchUser or matchEmail validation rule if present $this->wire('session')->remove($this->getAttribute('id') . '-email'); $this->wire('session')->remove($this->getAttribute('id') . '-username'); // finally add the files including the overwritten fielnames to the array if ($this->storedFiles) { $this->uploaded_files = $this->storedFiles; } return true; } else { // set error alert $this->wire('session')->set('errors', '1'); $this->formErrors = $v->errors(); $this->alert->setCSSClass('alert_dangerClass'); $this->alert->setText($this->getErrorMsg()); // add a max attempts warning message to the error message if ($this->getMaxAttempts() && isset($this->wire('session')->attempts)) { $attemptDiff = $this->getMaxAttempts() - $this->wire('session')->attempts; if ($attemptDiff <= 3) { $plural = $this->_('attempts'); $singular = $this->_('attempt'); $attempts = $this->_n($singular, $plural, $attemptDiff); $attemptWarningText = '<br>' . sprintf($this->_('You have %s %s left until you will be blocked due to security reasons.'), $attemptDiff, $attempts); $this->alert->setText($this->alert->getText() . $attemptWarningText); } } // create session for max attempts if set, otherwise add 1 attempt. //this session contains the number of failed attempts and will be increased by 1 on each failed attempt if ($this->getMaxAttempts()) { $this->wire('session')->attempts += 1; // increase session on each invalid attempt if (($this->getMaxAttempts() - $this->wire('session')->attempts) == 0) { $this->alert->setCSSClass('alert_warningClass'); $this->alert->setText(sprintf($this->_('This is failed attempt number %s. This is your last attempt to send the form. After that you will be blocked due to security reasons.'), ($this->wire('session')->attempts))); } } else { // remove the session for attempts if set to 0 $this->wire('session')->remove('attempts'); } return false; } /* END PROCESSING THE FORM */ } // CSRF attack die(); // live a great life and die() gracefully. } //double form submission return false; } //max attempts were reached return false; } // submission time was too short or to long return false; } // this form was not submitted return false; } /** * Create a honeypot field for spam protection * @return InputText */ private function createHoneypot(): InputText { $honeypot = new InputText('seca'); $honeypot->setAttribute('name', $this->createElementName('seca')); $honeypot->setLabel($this->_('Please do not fill out this field'))->setAttribute('class', 'seca'); // Remove or add wrappers depending on settings $honeypot->useInputWrapper($this->useInputWrapper); $honeypot->useFieldWrapper($this->useFieldWrapper); $honeypot->getFieldWrapper()->setAttribute('class', 'seca'); $honeypot->getInputWrapper()->setAttribute('class', 'seca'); $honeypot->setAttributes(['class' => 'seca', 'tabindex' => '-1']); return $honeypot; } /** * Add the input wrapper to all fields of this form in general * @param bool $useInputWrapper * @return void */ public function useInputWrapper(bool $useInputWrapper): void { $this->useInputWrapper = $useInputWrapper; } /** * Add the field wrapper to all fields of this form in general * @param bool $useFieldWrapper * @return void */ public function useFieldWrapper(bool $useFieldWrapper): void { $this->useFieldWrapper = $useFieldWrapper; } /** * Internal method to add all form values to the values array * All sanitizers applied to an input element will be used before they will be added to the array * @return void */ private function setValues(): void { $values = []; foreach ($this->formElements as $element) { if ($element->getAttribute('value')) { // Run all sanitizer methods over the value if (method_exists($element, 'getSanitizers')) { $sanitizers = $element->getSanitizers(); foreach ($sanitizers as $sanitizer) { $value = $element->wire('sanitizer')->$sanitizer($element->getAttribute('value')); } } else { $value = $element->getAttribute('value'); } $values[$element->getAttribute('name')] = $value; // set all form values to a placeholder $fieldName = str_replace($this->getID() . '-', '', $element->getAttribute('name')) . 'value'; $this->setMailPlaceholder($fieldName, $element->getAttribute('value')); } } $this->values = $values; } /** * Get the success message * @return string */ protected function getSuccessMsg(): string { return $this->frontendforms['input_alertSuccessText']; } /** * Get the error message * @return string */ protected function getErrorMsg(): string { return $this->frontendforms['input_alertErrorText']; } /** * Get the max attempts * @return int */ public function getMaxAttempts(): int { return $this->frontendforms['input_maxAttempts']; } /** * Set the max attempts * @param int $maxAttempts * @return void */ public function setMaxAttempts(int $maxAttempts): void { if ($maxAttempts < 1) { $this->frontendforms['input_logFailedLogins'] = 0; } //disable logging of failed attempts $this->frontendforms['input_maxAttempts'] = $maxAttempts; } /** * Method to run if a user has taken too many attempts * This method has to be before the render method of the form * You can use it fe to save some data to the database -> you got the idea * @return bool -> returns true if the user is blocked, otherwise false * @throws WireException */ public function isBlocked(): bool { if ($this->wire('session')->get('blocked')) { return true; } return false; } /** * Set a redirect url after the form has been submitted successfully via Ajax * This forces a Javascript redirect after the form has been validated without errors * @param string|null $url * @return $this */ public function setRedirectUrlAfterAjax(string|null $url = null): self { if (!is_null($url)) { $this->ajaxRedirect = $url; } return $this; } /** * Set the URL for a redirect after successful form validation * @param string $url - the URL, where the redirect should go to * @return $this */ public function setRedirectURL(string $url): self { $this->setRedirectUrlAfterAjax($url); $this->redirectURL = $url; return $this; } /** * Get the URL for a redirect if set, otherwise NULL * @return string|null */ protected function getRedirectURL(): string|null { return $this->redirectURL; } /** * Check if the form contains an element from the given class * @param string $className * @return int -> the number of fields found */ public function formContainsElementByClass(string $className): int { $className = wireClassNamespace($className, true); $number = (count(array_filter($this->formElements, function ($entry) use ($className) { return ($entry instanceof $className); }))); return $number; } /** * If there are multiple instances of a given class, remove all except the last one * This is useful if only one instance is allowed, but there are multiple instances * Returns the key of the last item, which will not be deleted (unset) * @param string $className * @return int */ public function removeMultipleEntriesByClass(string $className): null|int { // there are too many PrivacyText elements, only one is allowed per form -> remove all except the last one $elements = $this->getElementsbyClass($className); $k = null; if ($elements) { foreach ($elements as $key => $e) { if ($key !== array_key_last($elements)) { unset($this->formElements[key($e)]); } else { $k = key($e); } } } return $k; } /** * Get all form element objects of a given class as an array * @param string $className * @return array */ public function getElementsbyClass(string $className): array { $elements = []; if ($this->formContainsElementByClass($className)) { $className = wireClassNamespace($className, true); $elements[] = array_filter($this->formElements, function ($entry) use ($className) { return ($entry instanceof $className); }); } return $elements; } /** * Move an array item from one to another position * @param array $array * @param $key * @param int $order * @return void * @throws \Exception */ protected function repositionArrayElement(array &$array, $key, int $order): void { if (($a = array_search($key, array_keys($array))) === false) { throw new \Exception("The {$key} cannot be found in the given array."); } $p1 = array_splice($array, $a, 1); $p2 = array_splice($array, 0, $order); $array = array_merge($p2, $p1, $array); } /** * Render the form markup (including alerts if present) on the frontend * @return string * @throws WireException * @throws \Exception */ public function render(): string { // redirect after successful form validation if set if ($this->getRedirectURL() && $this->validated && !$this->getSubmitWithAjax()) { $this->wire('session')->redirect($this->getRedirectURL()); } $out = ''; // if Ajax submit was selected, add an aditional data attribute to the form tag if ($this->getSubmitWithAjax()) { // check if a user has Javascript enabled, otherwise show a warning message inside an alert box $warningAlert = new Alert(); $warningAlert->setCSSClass('alert_warningClass'); $warningAlert->prepend('<noscript>'); $warningAlert->append('</noscript>'); $warningAlert->setText($this->_('You do not have Javascript enabled. This could cause problems. Please enable Javascript to submit the form without any problems.')); $out .= $warningAlert->___render(); $this->setAttribute('data-submitajax', $this->getID()); // add special div container for Ajax form submission $out .= '<div id="' . $this->getID() . '-ajax-wrapper" data-validated="' . $this->validated . '">'; } // Check if the form contains file upload fields, then add enctype attribute foreach ($this->formElements as $obj) { if ($obj instanceof InputFile) { $this->setAttribute('enctype', 'multipart/form-data'); break; } } if (!$this->allowFormViewByIP()) { $this->alert->setCSSClass('alert_warningClass'); $this->alert->setText(sprintf($this->_('We are sorry, but your IP address %s is on the list of forbidden IP addresses. Therefore the form will not be displayed. If you think your IP address is mistakenly on the list, please contact the administrator of the site.'), $this->visitorIP)); // do not display form to banned visitors $this->showForm = false; } $out .= $this->prepend; $out .= $this->append; // allow only get or post - if value is not get or post set post as default value if (!in_array(strtolower($this->getAttribute('method')), Form::FORMMETHODS)) { $this->setAttribute('method', 'post'); } // get token for CSRF protection $tokenName = $this->wire('session')->CSRF->getTokenName(); $tokenValue = $this->wire('session')->CSRF->getTokenValue(); // remove all instances of form elements where only one instance per form is allowed, but there are multiple $singleClassObjects = ['PrivacyText', 'Privacy']; foreach ($singleClassObjects as $className) { $this->removeMultipleEntriesByClass($className); } // reindex array $this->formElements = array_values($this->formElements); $buttons = $this->getElementsbyClass('Button'); // get first button if ($buttons) { $refKey = key($buttons[0]); // add captcha field as last element before the button element if ($this->getCaptchaType() != 'none') { // position in form fields array to insert $captchaPosition = $refKey; $captchafield = $this->getCaptcha()->createCaptchaInputField($this->getID()); // insert the captcha input field after the last input field $this->formElements = array_merge(array_slice($this->formElements, 0, $captchaPosition), array($captchafield), array_slice($this->formElements, $captchaPosition)); // re-index the formElements array $this->formElements = array_values($this->formElements); } // sort the privacy elements that checkbox is before text, if both will be used $privacyElements = []; $privacyCheckbox = $this->getElementsbyClass('Privacy'); if ($privacyCheckbox) { $privacyElements[] = key($privacyCheckbox[0]); } $privacyText = $this->getElementsbyClass('PrivacyText'); if ($privacyText) { $privacyElements[] = key($privacyText[0]); } // get the position of the first button element if ($this->getElementsbyClass('Button')) { $firstButtonPos = key($this->getElementsbyClass('Button')[0]); if ($privacyElements) { sort($privacyElements); $newPos = $firstButtonPos - 1; $this->repositionArrayElement($this->formElements, $privacyElements[0], $newPos); if (array_key_exists(1, $privacyElements)) { $newPos = array_key_last($this->formElements) - 1; $this->repositionArrayElement($this->formElements, $privacyElements[1] - 1, $newPos); } } } } // create new array of inputfields only to position the honepot field in between $inputfieldKeys = []; foreach ($this->formElements as $key => $element) { if (is_subclass_of($element, 'FrontendForms\Inputfields')) { // exclude hidden input fields - add only visible fields if ($element->className() !== 'InputHidden') { $inputfieldKeys[] = $key; } } } // add honeypot on the random number field position if (($this->frontendforms['input_useHoneypot']) && ($inputfieldKeys)) { shuffle($inputfieldKeys); array_splice($this->formElements, $inputfieldKeys[0], 0, [$this->createHoneypot()]); } // create hidden Ajax redirect input if set // this value can be grabbed afterwards via Javascript to make a JS redirect if ($this->getSubmitWithAjax()) { if ($this->ajaxRedirect) { $ajaxredirectField = new InputHidden('ajax_redirect'); $ajaxredirectField->setAttribute('value', $this->ajaxRedirect); $this->add($ajaxredirectField); } } //create CSRF hidden field and add it to the form at the end $hiddenField = new InputHidden('post_token'); $hiddenField->setAttribute('name', $tokenName); $hiddenField->setAttribute('value', $tokenValue); $this->add($hiddenField); //create hidden field to prevent double form submission if it was not disabled if ($this->useDoubleFormSubmissionCheck) { $hiddenField2 = new InputHidden('doubleSubmission_token'); $hiddenField2->setAttribute('name', 'doubleSubmission_token'); $hiddenField2->setAttribute('value', $this->doubleSubmission); $this->add($hiddenField2); } //create hidden field to send form id to check if this form was submitted //this is only there for the case if other forms are present on the same page $hiddenField3 = new InputHidden('form_id'); $hiddenField3->setAttribute('name', 'form_id'); $hiddenField3->setAttribute('value', $this->getID()); $this->add($hiddenField3); //create hidden field to send the timestamp (encoded) when the form was loaded if (($this->getMinTime()) || $this->getMaxTime()) { $hiddenField4 = new InputHidden('load_time'); $hiddenField4->setAttribute('value', self::encryptDecrypt((string)time())); $this->add($hiddenField4); } /* BLOCKING ALERTS */ if ($this->wire('session')->get('blocked')) { // set danger alert for blocking messages $this->alert->setCSSClass('alert_dangerClass'); // return blocking text for too many failed attempts if ($this->wire('session')->get('blocked') == 'maxAttempts') { if ($this->wire('session')->get('attempts') == $this->getMaxAttempts()) { $this->alert->setText($this->_('You have reached the max. number of allowed attempts and therefore you cannot submit the form once more. To reset the blocking and to submit the form anyway you have to close this browser, open it again and visit this page once more.')); } } } // Output the form markup $out .= $this->alert->___render(); // render the alert box on top for success or error message // show form only if user is not blocked if ($this->showForm && (($this->wire('session')->get('blocked') == null))) { //add required texts $this->prepend($this->renderRequiredText('top')); // required text hint at the top $this->append($this->renderRequiredText('bottom')); // required text hint at bottom $formElements = ''; $elementsClassNames = (array_map("get_class", $this->formElements)); $position = array_search('FrontendForms\Button', $elementsClassNames); foreach ($this->formElements as $key => $element) { //create input ID as a combination of form id and input name $oldId = $element->getAttribute('id'); $element->setAttribute('id', $this->getID() . '-' . $oldId); // change the name attribute of the CSRF field if ($element->getID() == $this->getID() . '-post_token') { $element->setAttribute('name', $tokenName); } // Label and description (Only on input fields) if (is_subclass_of($element, 'FrontendForms\Inputfields')) { // set the description position on per form base if description text is present if ($element->getDescription()->getText()) { // set position from form setting if no individual position has been set if (is_null($element->getDescription()->getPosition())) { $element->getDescription()->setPosition($this->general_desc_position); } } // add unique id to the field-wrapper if present $element->getFieldWrapper()->setAttribute('id', $this->getID() . '-' . $oldId . '-fieldwrapper'); // add unique id to the input-wrapper if present $element->getInputWrapper()->setAttribute('id', $this->getID() . '-' . $oldId . '-inputwrapper'); $element->getLabel()->setAttribute('for', $element->getAttribute('id')); } $name = $element->getAttribute('id'); //Enable/disable wrap of the checkboxes by its label tag by appending the label after the input tag // by using the appendLabel() method if (($element instanceof InputCheckbox) || ($element instanceof InputCheckboxMultiple)) { $element->appendLabel($this->getAppendLabelOnCheckboxes()); } if (($element instanceof InputRadio) || ($element instanceof InputRadioMultiple)) { $element->appendLabel($this->getAppendLabelOnRadios()); } //add the form id as prefix to name attributes of multiple radios and checkboxes if (($element instanceof InputCheckboxMultiple) || ($element instanceof InputRadioMultiple)) { foreach ($element->getOptions() as $cb) { $brackets = ($element instanceof InputCheckboxMultiple) ? '[]' : ''; $cb->setAttribute('name', $name . $brackets); } } // add an element (progressbar, text,...) before the first button element for Ajax submit if ($this->getSubmitWithAjax()) { if ($key === $position) { // add it only before the first button inside the form if ($this->showProgressbar) { // create progressbar and info text for form submission $submitInfo = new Alert(); $submitInfo->setCSSClass('alert_primaryClass'); $submitInfo->setContent($this->createProgressbar() . $this->_('Please be patient... the form will be validated!')); $formElements .= '<div id="' . $this->getID() . '-form-submission" class="progress-submission" style="display:none">' . $submitInfo->___render() . '</div>'; } } } if (array_key_exists($name, $this->formErrors)) { $element->setCSSClass('input_errorClass'); // add Aria attributes if($this->useAriaAttributes){ $element->setAttribute('aria-invalid','true'); $element->setAttribute('aria-errormessage',$element->getID().'-errormsg'); } // set error class for input element $element->setErrorMessage($this->formErrors[$name][0])->setAttribute('id', $element->getID().'-errormsg'); //get a first error message } else { if($this->useAriaAttributes && $this->isSubmitted()){ $element->setAttribute('aria-invalid','false'); } } $formElements .= $element->render() . PHP_EOL; } // add formElementsWrapper -> add the div container after the form tag if ($this->frontendforms['input_wrapperFormElements']) { $this->getformElementsWrapper()->setContent($formElements); $formElements = $this->formElementsWrapper->___render() . PHP_EOL; } // render the form with all its fields $this->setContent($formElements); $out .= $this->renderNonSelfclosingTag($this->getTag()); } if ($this->getSubmitWithAjax()) { $out .= '</div>'; } return $out; } /** * Append a field object to the form * @param object $field - object of inputfield, fieldset, button, ... * @return void */ /** * Append a field object to the form * The 2 optional parameters are only for the creation of 2 new methods: addBefore() and addAfter() * These 2 methods can be used to add new form elements (inputs, text elements, fieldsets,…) to a formElements * array at a certain position These 2 methods are especially designed for the future usage in module dev - no * need to use it if you are creting the form by your own * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\Button|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose $field - * the current form field which should be appended to the form * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\Button|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose|bool|null $otherfield - * optional: another form field * @param bool $add_before - optional: current should be inserted before or after this (another) form field * @return void * @throws \Exception */ public function add( Inputfields|Textelements|Button|FieldsetOpen|FieldsetClose $field, Inputfields|Textelements|Button|FieldsetOpen|FieldsetClose|null|bool $otherfield = null, bool $add_before = false ): void { // add or remove wrapper divs on each form element if (is_subclass_of($field, 'FrontendForms\Inputfields')) { $field->useInputWrapper($this->useInputWrapper); $field->useFieldWrapper($this->useFieldWrapper); // create a placeholder for the label of this field $fieldname = $field->getAttribute('name'); $this->setMailPlaceholder($fieldname . 'label', $field->getLabel()->getText()); $this->setMailPlaceholder($fieldname . 'value', $field->getAttribute('value')); } // if the field is not a text element, set the name attribute if (!is_subclass_of($field, 'FrontendForms\TextElements')) { // Add id of the form as prefix for the name attribute of the field $field->setAttribute('name', $this->getID() . '-' . $field->getId()); } if (!is_null($otherfield)) { // check if another field exists if (is_bool($otherfield)) { throw new Exception("The reference field (argument 2) where you want to add this field before or after does not exist. Please check if you have written the name attribute correctly.", 1); } else { // check if the field with this id exists inside the formElements array if ($this->getFormelementByName($otherfield->getAttribute('name'))) { $ref_position = null; // get the key of this field inside the formElements array $this->formElements = array_values($this->formElements); foreach ($this->formElements as $key => $element) { if ($element == $otherfield) { $ref_position = $key; } } // insert field to the new position if (is_int($ref_position)) { if (!$add_before) { // add after $ref_position = $ref_position + 1; } $this->formElements = array_merge(array_slice($this->formElements, 0, $ref_position), [$field], array_slice($this->formElements, $ref_position)); } } } } else { // no other element is present -> so add it to formElements array as next element $this->formElements = array_merge($this->formElements, [$field]); // array must be numeric for honeypot field } } /** * Insert a form field before another form field * Can be used if you have not created the form by your own, but you need to add a new field to a created * formElements array at a certain position * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\Button|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose $field - * the current form field * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose|\FrontendForms\Button|bool $before_field - * the form field object before which the current form field object should be inserted * @return void * @throws \Exception */ public function addBefore( Inputfields|Textelements|Button|FieldsetOpen|FieldsetClose $field, Inputfields|Textelements|FieldsetOpen|FieldsetClose|Button|bool $before_field ): void { // if a field is present inside the formelements array, remove it first if (($field->getAttribute('name')) && ($this->getFormelementByName($field->getAttribute('name')))) { $this->remove($field); } $this->add($field, $before_field, true); } /** * Insert a form field after another form field * Can be used if you have not created the form by your own, but you need to add a new field to a created * formElements array at a certain position * * * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\Button|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose $field - * the current form field * @param \FrontendForms\Inputfields|\FrontendForms\Textelements|\FrontendForms\FieldsetOpen|\FrontendForms\FieldsetClose|\FrontendForms\Button|bool $after_field - * the form field object after which the current form field object should be inserted * @return void * @throws \Exception */ public function addAfter( Inputfields|Textelements|Button|FieldsetOpen|FieldsetClose $field, Inputfields|Textelements|FieldsetOpen|FieldsetClose|Button|bool $after_field ): void { // if a field is present inside the formelements array, remove it first if (($field->getAttribute('name')) && ($this->getFormelementByName($field->getAttribute('name')))) { $this->remove($field); } $this->add($field, $after_field); } /** * Remove a form field from the fields array * @param object $field * @return void */ public function remove(object $field): void { if (($key = array_search($field, $this->formElements)) !== false) { unset($this->formElements[$key]); // remove the placeholders too if they are present $fieldname = $field->getAttribute('name'); $this->removePlaceholder(strtoupper($fieldname . 'label')); $this->removePlaceholder(strtoupper($fieldname . 'value')); } } /** * Get the min time value * @return int */ public function getMinTime(): int { return $this->frontendforms['input_minTime']; } /** * Set the min time in seconds before the form should be submitted * @param int $minTime * @return $this */ public function setMinTime(int $minTime): self { $this->frontendforms['input_minTime'] = $minTime; return $this; } /** * Get the max time value * @return int */ protected function getMaxTime(): int { return $this->frontendforms['input_maxTime']; } /** * Set the max time in seconds until the form should be submitted * @param int $maxTime * @return $this */ public function setMaxTime(int $maxTime): self { $this->frontendforms['input_maxTime'] = $maxTime; return $this; } /** Static method to encrypt/decrypt a string according to the encryption settings * @param string $string * @param string $method * @return string */ public static function encryptDecrypt(string $string, string $method = 'encrypt'): string { // encryption settings $encrypt_method = 'AES-256-CBC'; $secret_key = 'd0a7e7997b6d5fcd55f4b5c32611b87cd923e88837b63bf2941ef819dc8ca282'; $secret_iv = '5fgf5HJ5g27'; $algo = 'sha256'; // user define secret key $key = hash($algo, $secret_key); $iv = substr(hash($algo, $secret_iv), 0, 16); $methods = ['encrypt', 'decrypt']; if (in_array($method, $methods)) { if ($method === 'encrypt') { $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv); return base64_encode($output); } else { return openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv); } } return $string; } /** * Create a required hint text element if showTextHint is set to true * @param string $position - has to be 'top' or 'bottom' * @return string */ private function renderRequiredText(string $position): string { if ($this->defaultRequiredTextPosition === $position) { return $this->requiredHint->___render(); } return ''; // return empty string } /** * Create a random string with a certain length for usage in URL query strings * @param int $charLength - the length of the random string - default is 100 * @return string - returns a slug version of the generated random string that can be used inside an url */ protected function createQueryCode(int $charLength = 100): string { $pass = new \ProcessWire\Password(); if ($charLength <= 0) { $charLength = 10; } // instantiate a password object to use the methods $string = $pass->randomBase64String($charLength); return $this->generateSlug($string); } /** * Generate a slug out of a string for usage in urls (fe query strings) * This is only a helper function * @param $string - the string * @return string */ protected function generateSlug(string $string): string { return preg_replace('/[^A-Za-z\d-]+/', '-', $string); } /** * Make a readable string from a number of seconds * @param int $seconds - a number of seconds which should be converted to a readable string * @return string|null - a readable string of the time (fe 1 day instead of 86400 seconds) * @throws Exception */ protected function readableTimestringFromSeconds(int $seconds = 0): ?string { $then = new DateTime(date('Y-m-d H:i:s', 0)); $now = new DateTime(date('Y-m-d H:i:s', $seconds)); $interval = $then->diff($now); if ($interval->y >= 1) { $thetime[] = $interval->y . ' ' . _n($this->_('year'), $this->_('years'), $interval->y); } if ($interval->m >= 1) { $thetime[] = $interval->m . ' ' . _n($this->_('month'), $this->_('months'), $interval->m); } if ($interval->d >= 1) { $thetime[] = $interval->d . ' ' . _n($this->_('day'), $this->_('days'), $interval->d); } if ($interval->h >= 1) { $thetime[] = $interval->h . ' ' . _n($this->_('hour'), $this->_('hours'), $interval->h); } if ($interval->i >= 1) { $thetime[] = $interval->i . ' ' . _n($this->_('minute'), $this->_('minutes'), $interval->i); } if ($interval->s >= 1) { $thetime[] = $interval->s . ' ' . _n($this->_('second'), $this->_('seconds'), $interval->s); } return isset($thetime) ? implode(' ', $thetime) : null; } /** * Return the names of all input fields inside a form as an array * @return array */ public function getNamesOfInputFields(): array { $elements = []; if ($this->formElements) { foreach ($this->formElements as $element) { if (is_subclass_of($element, 'FrontendForms\Inputfields')) { $elements[] = $element->getAttribute('name'); } } } return array_filter($elements); } /** * Output an error message that email could not be sent due to possible wrong email configuration settings * This is a general message that could be used for all forms * @return void */ protected function generateEmailSentErrorAlert(): void { $this->alert->setCSSClass('alert_dangerClass'); $this->alert->setText($this->_('Email could not be sent due to possible wrong email configuration settings.')); } /** * Return placeholders for email pre-header to prevent showing up other text * The Litmus hack adds empty spaces after the mail placeholder to prevent the display of other text inside the * pre-header * @return string */ protected function getLitmusHack(): string { return '&#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279; &#847; &zwnj; &nbsp; &#8199; &#65279;'; } protected function getPreheaderStyle(): string { return 'display:none;font-size:1px; color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;'; } /** * Generate an invisible pre-header text after the subject for an email * @param \ProcessWire\WireMail $mail * @return string */ protected function generateEmailPreHeader(WireMail $mail): string { if ($mail->title) { // check if title property was set // generate an invisible div container return '<div id="preheader-text" style="' . $this->getPreheaderStyle() . '">' . $mail->title . $this->getLitmusHack() . '</div>'; } return ''; } } The Aria attributes will not be added by default, so you have to enable it on your form too: $form->useAriaAttributes(); After this changes the Aria attributes will displayed properly (only) after form submission. Instead of the aria-described-by I have used the aria-errormessage attribute, because I guess it is the better one for usage with error messages (Take a look here). Using an additional message if field has been filled out correctly, is not really necessary or makes not a better user experience in my opinion. <small id="valid-helper">Looks good!</small> So I am not sure if this is it worth to add it to the form class too, but it could be realized via a new setSuccessMessage() method which will be added to an input field. This method is not implemented yet, but could be. You can try my code above and if it works for you I can add it to the GitHub repositiory and it will be on the next update. Jürgen
  4. Hello. I'd somewhat rewritten WireMailMailgun a while back, to implement some more features and implement PW coding guidelines etc. My version has been somewhat in limbo, although there's been various discussions in the thread linked above. I've decided to release my version as a separate module called WireMailgun, as it has breaking changes and a slightly different implementation. I've also got it using the email validation v4 endpoint. For simple usage, either module will do what you need it to do. If you need to do some more advanced things e.g inline images or adding data, this module will help you with that. Here's the readme... WireMail Mailgun Extends WireMail to use the Mailgun API for sending emails. Installation Download the zip file at Github or clone the repo into your site/modules directory. If you downloaded the zip file, extract it in your sites/modules directory. In your admin, go to Modules > Refresh, then Modules > New, then click on the Install button for this module. API Prior to using this module, you must set up a domain in your Mailgun account to create an API key. Add the API key and domain to the module's settings. Usage Usage is similar to the basic WireMail implementation, although a few extra options are available. Please refer to the WireMail documentation for full instructions on using WireMail, and to the examples below. Extra Methods The following are extra methods implemented by this module: Chainable The following methods can be used in a chained statement: cc(string|array|null $email) - Set a "cc" email address. Only used when $batchMode is set to false. Please refer to WireMail::to() for more information on how to use this method. bcc(string|array|null $email) - Set a "bcc" email address. Only used when $batchMode is set to false. Please refer to WireMail::to() for more information on how to use this method. addData(string $key, string $value) - Add custom data to the email. See https://documentation.mailgun.com/en/latest/user_manual.html#attaching-data-to-messages for more information. addInlineImage(string $file, string $filename) - Add an inline image for referencing in HTML. Reference using "cid:" e.g. <img src='cid:filename.ext'> Requires curl_file_create() (PHP >= 5.5.0) See https://documentation.mailgun.com/en/latest/user_manual.html#sending-inline-images for more information. addRecipientVariables(array $recipients) - Add recipient variables. $recipients should be an array of data, keyed by the recipient email address See https://documentation.mailgun.com/en/latest/user_manual.html#batch-sending for more information. addTag(string $tag) - Add a tag to the email. Only ASCII allowed Maximum length of 128 characters There is a maximum number of 3 tags allowed per email. addTags(array $tags) - Add tags in a batch. setApiKey(string $apiKey) - Override the Mailgun API Key module setting. setBatchMode(bool $batchMode) - Enables or disables batch mode. This is off by default, meaning that a single email is sent with each recipient seeing the other recipients If this is on, any email addresses set by cc() and bcc() will be ignored Mailgun has a maximum hard limit of recipients allowed per batch of 1,000. Read more about batch sending. setDeliveryTime(int $time) - The (unix)time the email should be scheduled for. setDomainName(string $domain) - Override the "Domain Name" module setting. setRegion(string $region) - Override the "Region" module setting. Valid regions are "us" and "eu" Fails silently if an invalid region is passed setSender(string $domain, string $key) - Set a different API sender than the default. The third argument is $region which is optional A shortcut for calling setDomainName(), setApiKey() and setRegion() setTestMode(bool $testMode) - Override the "Test Mode" module setting. setTrackOpens(bool $trackOpens) - Override "Track Message Opens" module setting on a per-email basis. Open tracking only works for emails with bodyHTML() set setTrackClicks(bool $trackClicks) - Override "Track Message Clicks" module setting on a per-email basis. Click tracking only works for emails with bodyHTML() set Other send() - Send the email. Returns a positive number (indicating number of emails sent) or 0 on failure. validateEmail(string $email) - Validates a single address using Mailgun's address validation service. Returns an associative array. To return the response as an object, set the second argument to false For more information on what this method returns, see Mailgun's documentation. getHttpCode() - Get the API HTTP response code. A response code of 200 indicates a successful response Examples Basic Example Send an email: $mg = $mail->new(); $sent = $mg->to("user@domain.com") ->from("you@company.com") ->subject("Message Subject") ->body("Message Body") ->send(); Advanced Example Send an email using all supported WireMail methods and extra methods implemented by WireMailgun: $mg = $mail->new(); // WireMail methods $mg->to([ "user@domain.com" => "A User", "user2@domain.com" => "Another User", ]) ->from("you@company.com", "Company Name") ->replyTo("reply@company.com", "Company Name") ->subject("Message Subject") ->bodyHTML("<p>Message Body</p>") // A text version will be automatically created ->header("key1", "value1") ->headers(["key2" => "value2"]) ->attachment("/path/to/file.ext", "filename.ext"); // WireMailgun methods $mg->cc("cc@domain.com") ->bcc(["bcc@domain.com", "bcc2@domain.com"]) ->addData("key", "value") // Custom X-Mailgun-Variables data ->addInlineImage("/path/to/file-inline.jpg", "filename-inline.jpg") // Add inline image ->addTag("tag1") // Add a single tag ->addTags(["tag2", "tag3"]) // Add tags in a batch ->setBatchMode(false) // A single email will be sent, both "to" recipients shown ->setDeliveryTime(time() + 3600) // The email will be delivered in an hour ->setSender($domain, $key, "eu") // Use a different domain to send, this one in the EU region ->setTestMode(true) // Mailgun won't actually send the email ->setTrackOpens(false) // Disable tracking opens ->setTrackClicks(false); // Disable tracking clicks // Batch mode is set to false, so 1 returned if successful $numSent = $mg->send(); echo "The email was " . ($numSent ? "" : "not ") . "sent."; Validate an Email Address $mg = $mail->new(); $response = $mg->validateEmail("user@domain.com", false); if($mg->getHttpCode() == 200) { echo $response->result == "deliverable" ? "Valid" : "Not valid"; } else { echo "Could not validate"; } I hope it is useful! Cheers, Chris
  5. The module can generate basic ICS calendar strings and files. Usage Example: $icsgen = wire()->modules->IcsGenerator; // set properties $icsgen->setArray(array( 'date' => new \DateTime('2033-12-24 12:00'), 'dateEnd' => new \DateTime('2033-12-24 13:00'), 'summary' => 'Event title', 'description' => 'Event description', )); // get path to a temporary .ics file // (using wire()->files->tempDir) $icspath = $icsgen->getFile(); // send email with ics file $mail = wireMail(); $mail->attachment($icspath, 'calendar.ics'); $mail->to($user->email); $mail->subject('ICS Demo'); $mail->body('This is a ICS demo.'); $numSent = $mail->send(); For more infos see GitHub Readme or Modules Page. If you experience reproducable issues please open a GitHub issue.
  6. Sorry if this is posted in the wrong place, a search yielded 190 pages of results so not helpful. Anyway... I have changed the email address in WireMail Gmail but when I do a test its still using the old one, I cannot find any caches relevant, how do I make it use the new address? TIA
  7. I'm almost done with my integration of this module, and it's working great. WireMail and WireMailSMTP integration was easy. I have 2 issues though: 1. On AJAX submission, the loading message "Please be patient... the form will be validated!" seems to be hard-wired. Would you be able to offer some way for developers to override it with their own custom text? For example, I'd like my site to simply show "Submitting your enquiry...". 2. Whenever I type into my textarea form field, my JavaScript console gives me this error: Uncaught TypeError: Cannot read properties of null (reading 'children') at HTMLTextAreaElement.<anonymous> (frontendforms.js?v=2.2.17:455:29) I think it's something to do with the character counter, which I have not enabled. And the form still submits OK. Thanks in advance for looking into these issues!
  8. Minor request. Is it possible to use the 'test' function when wireMailRouter is installed? I get "Couldn't get the right WireMail-Module (WireMailSmtp). found: WireMailRouter" If I set $config->wireMail('module', 'WireMailSmtp'); temporarily then it works, but I wonder if it is possible for the module to supply that as an override when testing?
  9. Hi, I'm stucked while using the mail sending, also the plugin WireMailSmtp is installed. $m = wireMail(); $m->to('test@example.com') ->header('bcc', 'asdf@example.com') //->from() ->subject( "HMTL | ") ->bodyHTML("DAs ist ein TÄÖÜ") ->send(); is creating the mail attached. I have no clue, but I also do not want to use phpmailer Any help is appreciated!
  10. Hi, I don't have WireMailRouter and therefor don't know how it works together. But please can you (or @Ivan Gretsky or @wbmnfktr, who seems to have it too, like you) simply test the following OneLineChange in the WireMailSmtp-Testfunction? $mail = wireMail(); if ($mail->className != 'WireMailSmtp' && $mail->className != 'WireMailRouter) { .... It then will skip the abort. So, don't know if it then works like it should, but that's why I ask for the test. 🙂 Let me know if someone can do that, and if so, what was the result. Greetings! 🙋‍♂️
  11. Pete and I have been using Postmark in some PW based projects at reasonable scale (>13k emails a month) and have found it to be an exceptionally good API-based transactional email provider with fast delivery times and great availability. It seems strange that there is no WireMail offering (as far as we know of anyway) that supports Postmark, so we thought we'd throw one together in case anyone else in the community wants to give Postmark a try. NB: This is not the code we use in our production systems, just a rainy-day project to fill a gap in the WireMail ecosystem. However, it should be sufficient to get you going with Postmark. We hope you find it useful and please let us know if you find any issues. WireMailPostmark module on Netcarver's github account. Screenshot from my test account:
  12. Hi, here is the new code for your contact form: <?php declare(strict_types=1); namespace ProcessWire; /* * File description * * Created by Jürgen K. * https://github.com/juergenweb * File name: contactform-2.php * Created: 15.02.2023 */ $content = ''; $form = new \FrontendForms\Form('contact'); $form->setMaxAttempts(0); // disable max attempts $contacttype = new \FrontendForms\InputRadioMultiple('auswahl'); $contacttype->addOption('Einzelperson (Jahresbeitrag 50,00 EUR)<span class="asterisk">*</span>', 'Einzelperson (Jahresbeitrag 50,00 EUR)')->setAttribute('data-value', '1'); $contacttype->addOption('Doppelmitgliedschaft (Jahresbeitrag 75,00 EUR)<span class="asterisk">*</span>', 'Doppelmitgliedschaft (Jahresbeitrag 75,00 EUR)')->setAttribute('data-value', '2'); $contacttype->alignVertical(); $contacttype->setRule('required')->setCustomMessage('Bitte wählen Sie die gewünschte Mitgliedschaft aus.'); $form->add($contacttype); // add the gender field $gender = new \FrontendForms\Gender('gender'); //$gender->setLabel('Auswählen'); // so Label setzen! $form->add($gender); // add the name field $name = new \FrontendForms\Name('firstname'); $name->setRule('firstAndLastname')->setCustomFieldName('Der Vorname'); $form->add($name); // add the surname field $surname = new \FrontendForms\Surname('lastname'); $surname->setRule('firstAndLastname')->setCustomFieldName('Der Nachname'); $form->add($surname); // check if $_POST value is present for "auswahl" and set "display" according to "auswahl" value $display = 'none'; if (wire('input')->post('contact-auswahl')) { $value = explode(' ', wire('input')->post('contact-auswahl')); if ($value != 'Einzelperson') { $display = 'block'; } } // add the name2 field $name2 = new \FrontendForms\InputText('firstname2'); $name2->setLabel('Vorname (zweite Person)'); $name2->setRule('firstAndLastname')->setCustomFieldName('Der Vorname der zweiten Person'); $name2->useCustomWrapper()->setAttribute('id', 'firstname-wrapper')->setAttribute('style', 'display:' . $display); $form->add($name2); // add the surname2 field $surname2 = new \FrontendForms\InputText('lastname2'); $surname2->setLabel('Nachname (zweite Person)'); $surname2->setRule('firstAndLastname')->setCustomFieldName('Der Nachname der zweiten Person'); $surname2->useCustomWrapper()->setAttribute('id', 'lastname-wrapper')->setAttribute('style', 'display:' . $display); $form->add($surname2); // Adresse $adresse = new \FrontendForms\InputText('adresse'); $adresse->setLabel('Straße und Hausnummer'); $adresse->setRule('required')->setCustomFieldName('Die Adresse'); $form->add($adresse); // plz $plz = new \FrontendForms\InputText('plz'); $plz->setLabel('Postleitzahl'); $plz->setRule('integer')->setCustomFieldName('Die Postleitzahl'); $plz->setRule('required'); $form->add($plz); // Ort $ort = new \FrontendForms\InputText('ort'); $ort->setLabel('Ort'); $ort->setRule('required')->setCustomFieldName('Der Ort'); $form->add($ort); // add the email field $email = new \FrontendForms\Email('email'); if ($user->isLoggedIn()) { $email->setDefaultValue($user->email); } $form->add($email); // add the phone field $phone = new \FrontendForms\Phone('phone'); $phone->setLabel('Telefon / Mobil'); $phone->setRule('required')->setCustomFieldName('Die Telefonnummer'); $form->add($phone); // add the Geburtstags field $geb = new \FrontendForms\InputText('geb'); $geb->setLabel('Geburtsdatum'); $form->add($geb); $einzug = new \FrontendForms\InputCheckbox('einzug'); $einzug->setLabel('Hiermit ermächtige ich den Museumsverein Celle e.V. widerruflich, die von mir zu entrichtende Beitragszahlung jährlich bei Fälligkeit zu Lasten meines Kontos mittels Lastschrift einzuziehen. Wenn mein Konto die erforderliche Deckung nicht aufweist, besteht seitens des kontoführenden Geldinstituts keine Verpflichtung zur Einlösung.'); $einzug->setRule('required')->setCustomMessage('Bitte bestätigen Sie die Einzugsermächtigung.'); $form->add($einzug); // Kontoinhaber $kontoinhaber = new \FrontendForms\InputText('kontoinhaber'); $kontoinhaber->setLabel('Kontoinhaber'); $kontoinhaber->setRule('firstAndLastname')->setCustomFieldName('Der Name des Kontoinhabers'); $kontoinhaber->setRule('required')->setCustomMessage('Der Name des Kontoinhabers muss ausgefüllt werden.'); $form->add($kontoinhaber); // Kreditinstitut $kreditinstitut = new \FrontendForms\InputText('kreditinstitut'); $kreditinstitut->setLabel('Kreditinstitut'); $kreditinstitut->setRule('required')->setCustomFieldName('Das Kreditinstitut'); $form->add($kreditinstitut); // BIC $bic = new \FrontendForms\InputText('bic'); $bic->setLabel('BIC'); $bic->setRule('required')->setCustomFieldName('Der BIC-Code'); $form->add($bic); // IBAN $iban = new \FrontendForms\InputText('iban'); $iban->setLabel('IBAN'); $iban->setRule('required')->setCustomFieldName('Der IBAN-Code'); $form->add($iban); // add the privacy field $privacy = new \FrontendForms\Privacy('privacy'); $form->add($privacy); // add the send copy field $sendcopy = new \FrontendForms\SendCopy('sendcopy'); $form->add($sendcopy); $button = new \FrontendForms\Button('submit'); $button->setAttribute('value', 'Absenden'); $form->add($button); if ($form->isValid()) { /** You can use placeholders for the labels and the values of the form fields * This is the modern new way - only available at version 2.1.9 or higher * Big advantage: You do not have to use PHP code and there are a lot of ready-to-use placeholders containing fe the current date, the domain,..... * But it is up to you, if you want to use placeholders or do it the old way * */ $body = '[[TITLE]] [[AUSWAHLVALUE]]<br><br> [[GENDERVALUE]]<br> [[FIRSTNAMELABEL]]: [[FIRSTNAMEVALUE]]<br> [[LASTNAMELABEL]]: [[LASTNAMEVALUE]]<br>'; // check if "Doppelmitgliedschaft" has been selected - otherwise do not add these 2 fields to the mail body if (str_contains($form->getValue('auswahl'), 'Doppelmitgliedschaft')) { $body .= '[[FIRSTNAME2LABEL]]: [[FIRSTNAME2VALUE]]<br> [[LASTNAME2LABEL]]: [[LASTNAME2VALUE]]<br>'; } $body .= '[[ADRESSEVALUE]]<br> [[PLZVALUE]]&nbsp; [[ORTVALUE]]<br> [[EMAILVALUE]]<br> [[PHONELABEL]]: [[PHONEVALUE]]<br> [[GEBLABEL]]: [[GEBVALUE]]<br><br> [[KREDITINSTITUTLABEL]]: [[KREDITINSTITUTVALUE]]<br> [[KONTOINHABERLABEL]]: [[KONTOINHABERVALUE]]<br> [[BICLABEL]]: [[BICVALUE]]<br> [[IBANLABEL]]: [[IBANVALUE]]<br><br>'; // send the form with WireMail $m = wireMail(); if ($form->getValue('sendcopy')) { // send copy to sender $m->to($form->getValue('email')); } $m->to('mail@mail.de')// please change this email address to your own ->from($form->getValue('email')) ->subject('Ein neuer Mitgliedsantrag von ' . $form->getValue('firstname') . ' ' . $form->getValue('lastname')) ->title('<h1>Neuer Mitgliedsantrag</h1>') // this is a new property from this module ->bodyHTML($body) ->sendAttachments($form); if (!$m->send()) { $form->generateEmailSentErrorAlert(); // generates an error message if something went wrong during the sending process } } $content .= $form->render(); echo $content; I have added some additional validation rules to certain fields, some data attributes for the Javascript and some custom labels. Please replace this code with yours (but backup your code first ? ). To show/hide that name fields for the second person, you will need this little JavaScript snippet. <script> let auswahl = document.getElementsByName("contact-auswahl"); if(auswahl.length){ // add event listener to these radio buttons auswahl.forEach(function(elem) { elem.addEventListener("click", function() { let selected = elem.getAttribute('data-value'); let firstname = document.getElementById('firstname-wrapper'); let lastname = document.getElementById('lastname-wrapper'); if(selected == '2'){ firstname.style.display = 'block'; lastname.style.display = 'block'; } else { // hide the fields firstname.style.display = 'none'; lastname.style.display = 'none'; } }); }); } </script> Please add it to your template (fe before the closing body tag). That is all -> the form should work as expected. So please try it out. What I have not done is to make the name fields of the second name required if "Doppelmitgliedschaft" has been selected. It does not work with the "requiredWith" validator. So for this usecase a new validator has to be developed. Maybe I will try to develop this new validator and add it to the next update. Best regards
  13. Thank you so much for the detailed answer! I see, there is a lot to learn for me ? I did use and edited your example from github contactform-2.php: <?php declare(strict_types=1); namespace ProcessWire; /* * File description * * Created by Jürgen K. * https://github.com/juergenweb * File name: contactform-2.php * Created: 15.02.2023 */ $form = new \FrontendForms\Form('contact'); $form->setMaxAttempts(0); // disable max attempts //$contacttype = new \FrontendForms\InputCheckboxMultiple('auswahl'); $contacttype = new \FrontendForms\InputRadioMultiple('auswahl'); //$contacttype->setNotes('*'); $contacttype->addOption('Einzelperson (Jahresbeitrag 50,00 EUR)<span class="asterisk">*</span>', 'Einzelperson (Jahresbeitrag 50,00 EUR)'); $contacttype->addOption('Doppelmitgliedschaft (Jahresbeitrag 75,00 EUR)<span class="asterisk">*</span>', 'Doppelmitgliedschaft (Jahresbeitrag 75,00 EUR)'); $contacttype->alignVertical(); $contacttype->setRule('required'); $form->add($contacttype); // add the gender field $gender = new \FrontendForms\Gender('gender'); //$gender->setLabel('Auswählen'); // so Label setzen! $form->add($gender); // add the name field $name = new \FrontendForms\Name('firstname'); $form->add($name); // add the surname field $surname = new \FrontendForms\Surname('lastname'); $form->add($surname); // add the name2 field $name2 = new \FrontendForms\InputText('firstname2'); $name2->setLabel('Vorname (zweite Person)'); $form->add($name2); // add the surname2 field $surname2 = new \FrontendForms\InputText('lastname2'); $surname2->setLabel('Nachname (zweite Person)'); $form->add($surname2); // Adresse $adresse = new \FrontendForms\InputText('adresse'); $adresse->setLabel('Straße und Hausnummer'); $adresse->setRule('required'); $form->add($adresse); // plz $plz = new \FrontendForms\InputText('plz'); $plz->setLabel('Postleitzahl'); $plz->setRule('required'); $form->add($plz); // Ort $ort = new \FrontendForms\InputText('ort'); $ort->setLabel('Ort'); $ort->setRule('required'); $form->add($ort); // add the email field $email = new \FrontendForms\Email('email'); if ($user->isLoggedIn()) { $email->setDefaultValue($user->email); } $form->add($email); // add the phone field $phone = new \FrontendForms\Phone('phone'); $phone->setLabel('Telefon / Mobil'); $phone->setRule('required'); $form->add($phone); // add the Geburtstags field $geb = new \FrontendForms\Phone('geb'); $geb->setLabel('Geburtsdatum'); $form->add($geb); $einzug = new \FrontendForms\InputCheckbox('einzug'); $einzug->setLabel('Hiermit ermächtige ich den Museumsverein Celle e.V. widerruflich, die von mir zu entrichtende Beitragszahlung jährlich bei Fälligkeit zu Lasten meines Kontos mittels Lastschrift einzuziehen. Wenn mein Konto die erforderliche Deckung nicht aufweist, besteht seitens des kontoführenden Geldinstituts keine Verpflichtung zur Einlösung.'); $einzug->setRule('required')->setCustomMessage('Bitte bestätigen Sie die Einzugsermächtigung.');; $form->add($einzug); // Kontoinhaber $kontoinhaber = new \FrontendForms\InputText('kontoinhaber'); $kontoinhaber->setLabel('Kontoinhaber'); $kontoinhaber->setRule('required'); $form->add($kontoinhaber); // Kreditinstitut $kreditinstitut = new \FrontendForms\InputText('kreditinstitut'); $kreditinstitut->setLabel('Kreditinstitut'); $kreditinstitut->setRule('required'); $form->add($kreditinstitut); // BIC $bic = new \FrontendForms\InputText('bic'); $bic->setLabel('BIC'); $bic->setRule('required'); $form->add($bic); // IBAN $iban = new \FrontendForms\InputText('iban'); $iban->setLabel('IBAN'); $iban->setRule('required'); $form->add($iban); // add the privacy field $privacy = new \FrontendForms\Privacy('privacy'); $form->add($privacy); // add the send copy field $sendcopy = new \FrontendForms\SendCopy('sendcopy'); $form->add($sendcopy); $button = new \FrontendForms\Button('submit'); $button->setAttribute('value', 'Absenden'); $form->add($button); if ($form->isValid()) { /** You can grab the values with the getValue() method - this is the default (old) way */ /* $body = $m->title; $body .= '<p>Sender: '.$form->getValue('gender').' '. $form->getValue('firstname').' '.$form->getValue('lastname').'</p>'; $body .= '<p>Mail: '.$form->getValue('email').'</p>'; $body .= '<p>Subject: '.$form->getValue('subject').'</p>'; $body .= '<p>Message: '.$form->getValue('message').'</p>'; */ /** You can use placeholders for the labels and the values of the form fields * This is the modern new way - only available at version 2.1.9 or higher * Big advantage: You do not have to use PHP code and there are a lot of ready-to-use placeholders containing fe the current date, the domain,..... * But it is up to you, if you want to use placeholders or do it the old way * */ $body = '[[TITLE]] [[AUSWAHLVALUE]]<br><br> [[GENDERVALUE]]<br> [[FIRSTNAMELABEL]]: [[FIRSTNAMEVALUE]]<br> [[LASTNAMELABEL]]: [[LASTNAMEVALUE]]<br> [[FIRSTNAME2LABEL]]: [[FIRSTNAME2VALUE]]<br> [[LASTNAME2LABEL]]: [[LASTNAME2VALUE]]<br> [[ADRESSEVALUE]]<br> [[PLZVALUE]]&nbsp; [[ORTVALUE]]<br> [[EMAILVALUE]]<br> [[PHONELABEL]]: [[PHONEVALUE]]<br> [[GEBLABEL]]: [[GEBVALUE]]<br><br> [[KREDITINSTITUTLABEL]]: [[KREDITINSTITUTVALUE]]<br> [[KONTOINHABERLABEL]]: [[KONTOINHABERVALUE]]<br> [[BICLABEL]]: [[BICVALUE]]<br> [[IBANLABEL]]: [[IBANVALUE]]<br><br> // send the form with WireMail $m = wireMail(); if ($form->getValue('sendcopy')) { // send copy to sender $m->to($form->getValue('email')); } $m->to('mail@mail.de')// please change this email address to your own ->from($form->getValue('email')) ->subject('Ein neuer Mitgliedsantrag von ' . $form->getValue('firstname') . ' ' . $form->getValue('lastname')) ->title('<h1>Neuer Mitgliedsantrag</h1>') // this is a new property from this module ->bodyHTML($body) ->sendAttachments($form); if (!$m->send()) { $form->generateEmailSentErrorAlert(); // generates an error message if something went wrong during the sending process } } $content .= $form->render(); echo $content; When selecting the option for "Doppelmitgliedschaft" the fields $name2 ('firstname2') and $surname2 ('lastname2') should appear.
  14. <?php namespace Processwire; $contact_form = $pages->get("template=contact"); $company = $pages->get("template=company"); $form = new \FrontendForms\Form('Form'); $form->setHtml5Validation(true); // Enable HTML5 browser validation // First Name $firstname = new \FrontendForms\InputText('scf_firstname'); $firstname->setLabel($fields->get('scf_firstname')->$label); $firstname->setRule('required'); $form->add($firstname); // Last Name $lastname = new \FrontendForms\InputText('scf_lastname'); $lastname->setLabel($fields->get('scf_lastname')->$label); $lastname->setRule('required'); $form->add($lastname); // Phone $phone = new \FrontendForms\InputText('scf_phone'); $phone->setLabel($fields->get('scf_phone')->$label); $phone->setRule('required'); $form->add($phone); // Email $email = new \FrontendForms\InputText('scf_email'); $email->setLabel($fields->get('scf_email')->$label); $email->setRule('required'); $form->add($email); // SERVICES $service = new \FrontendForms\Select('scf_service'); $service->setLabel(__($fields->get('scf_service')->$label)); // Add options dynamically based on existing pages $services = wire('pages')->find('template=insurance|telephony|pension, sort=name'); // Add options dynamically based on existing pages foreach ($services as $servicePage) { $service->addOption($servicePage->title, $servicePage->id); } $form->add($service); $message = new \FrontendForms\Textarea('scf_message'); $message->setLabel($fields->get('scf_message')->$label); $message->setRule('required'); $form->add($message); $button = new \FrontendForms\Button('submit'); $button->setAttribute('value', $fields->get('label_send')->label); $button->setAttribute('class', 'btn btn-primary'); // Add the CSS class $form->add($button); if($form->isValid()){ $p = new Page(); $p->template = 'contact_inquiry'; $p->parent = wire('pages')->get('template=contact'); $p->title = $form->getValue('scf_firstname') . ' ' . $form->getValue('scf_lastname'); $p->scf_firstname = $form->getValue('scf_firstname'); $p->scf_lastname = $form->getValue('scf_lastname'); $p->scf_phone = $form->getValue('scf_phone'); $p->scf_email = $form->getValue('scf_email'); $p->scf_service = $form->getValue('scf_service'); $p->scf_message = $form->getValue('scf_message'); // Set the scf_date field to the current date and time with the correct timezone $dateTime = new \DateTime('now', new \DateTimeZone('Europe/Stockholm')); $p->scf_date = $dateTime->format('Y-m-d H:i:s'); $p->save(); // Retrieve the title of the selected legal service $selectedserviceID = $form->getValue('scf_service'); $selectedservice = wire('pages')->get($selectedserviceID); $selectedserviceTitle = $selectedservice ? $selectedservice->title : 'Not specified'; // Initialize $messageBody $messageBody = ''; $m = wireMail(); $m->to('test@gmail.com'); $m->subject("{$contact_form->scf_email_title} {$selectedserviceTitle}, {$form->getValue('scf_firstname')} {$form->getValue('scf_lastname')}"); // Set "Reply-To" address $m->header('Reply-To', $form->getValue('scf_email') . ' (' . $form->getValue('scf_firstname') . ' ' . $form->getValue('scf_lastname') . ')'); $messageBody .= "<strong>{$fields->get('scf_firstname')->label}:</strong> " . $form->getValue('scf_firstname') . "<br>"; $messageBody .= "<strong>{$fields->get('scf_lastname')->label}:</strong> " . $form->getValue('scf_lastname') . "<br>"; $messageBody .= "<strong>{$fields->get('scf_phone')->label}:</strong> " . $form->getValue('scf_phone') . "<br>"; $messageBody .= "<strong>{$fields->get('scf_email')->label}:</strong> " . $form->getValue('scf_email') . "<br><br>"; $messageBody .= "<strong>{$fields->get('scf_service')->label}:</strong> " . $selectedserviceTitle . "<br><br>"; $messageBody .= "<strong>{$fields->get('scf_message')->label}</strong><br>" . $form->getValue('scf_message') . "<br>"; $m->bodyHTML($messageBody); $m->send(); // Set up the email for the form submitter $confirmationEmail = wireMail(); $confirmationEmail->to($form->getValue('scf_email')); $confirmationEmail->subject("{$contact_form->scf_email_confirmation_title} {$selectedserviceTitle}"); $confirmationMessage = "{$contact_form->scf_email_confirmation_message}"; $confirmationEmail->bodyHTML($confirmationMessage); $confirmationEmail->send(); // Redirect to the success page wire('session')->redirect(wire('config')->urls->root . 'arendet-skickades/'); } echo $form->render(); ?> When i insert like XX into the email field there is an error Error: Exception: Invalid email address (in E:\Wamp\www\website\wire\core\WireMail.php line 162). I dont know how to validate the email adress before sending the form. Any clues?
  15. Trying to work out why my module is send two emails when it should be sending one. I have a log statement at the end, it only shows up once in the logs where as i'm still recieving two emails!!? Which is why I'm confused: $mail = wireMail(); // set a default email address if none set $mail->to($u->email)->from($this->siteEmail); // all calls can be chained $mail->subject('New Unread message'); $mail->bodyHTML($msg); $numSent = $mail->send(); if($numSent) $this->wire('log')->save('mm_emails', 'New email sent to: ' . $u->email); I'm running PW 3.0.165
  16. Hi! I've got an issue with the Wiremail SMTP module. The settings test in the module works without problems. But not the mails send via the website. In the logs "wiremailsmtp_errors" i always got this strange message: Error in hnsmtp::send : 250 OK What does that mean? Where or how can i get more infos or details errors? The background: I send mails everytime a page is published to all users via the ready.php . The mail are sent to my mail adress with all the user mail adresses in bcc. Thanks for any ideas & cheers!
  17. Hey guys, Microsoft is deactivating SMTP Basic Authentication at the end of the year. https://learn.microsoft.com/en-gb/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online Has anyone already found a solution to get Office365 working with OAuth 2.0? @ryan would it be possible to adapt your "WireMailGmail" module? Or maybe someone is knowledgeable enough to design a general OAuth wireMail module? ?
  18. @Sanyaissues Thanks for reporting. I must admit, I'm stumped by this as the PHP looks OK to me - it's a simple string concatenation. Perhaps @ryan can chip in and let us know if the module loading code tries to find the PHP string by pattern matching on the text of the module file, rather than running the function - though I doubt that's the case. Anyway, could you try replacing the getModuleInfo() function with the following in the WiremailPostmark.module.php file... public static function getModuleInfo() { $php_version = 'PHP>=' . self::MIN_PHP; return [ 'name' => self::MYCLASS, 'title' => 'WireMail for Postmark', 'author' => 'Netcarver & Pete of Nifty Solutions', 'summary' => 'Allows Processwire to send transactional email via Postmark', 'href' => 'https://postmarkapp.com', 'version' => '0.5.3', 'autoload' => true, 'singular' => false, 'permanent' => false, 'requires' => [ 'ProcessWire>3.0.33', $php_version, ], ]; } ...and try reloading and installing the module? Please post a screenshot of the errors (if any). Also, what version of ProcessWire are you running? Thanks.
  19. Hi @netcarver,I was testing some WireMail integrations and it wasn't possible to install WiremailPostmark: Requires ProcessWire > 3.0.33 PHP 8.1.212.17 >= .selfMIN_PHP Thanks.
  20. Support thread for WireMail Mailgun. For bug reports, please use the bug tracker on GitHub. https://github.com/plauclair/WireMailMailgun
  21. Hello @mayks I have installed the module and I have sent 2 mails with it (I have clicked the "Password forgotten" link to send me a mail, where I can change my password). From the statistic data, it seems to work (only the "Sent" will be displayed, but I guess this is because I have the trial plan). I have tested it only on the "Password forgotten" page, but at the first sight, it seems that only the mail object instantiation is a little bit different than like the WireMail, but all other methods are equal. I cannot promise at 100% that I will implement it and if so I do not know when I will find the time (not today and not tomorrow), but it seems not to be so complicated. I did not know this module or this mail service before and personally I will not use it, because I do not want to pay for it, but it could be interesting for users, who want a statistic. My idea: If I implement it, I would add a Select option inside the module configuration with the options "WireMail" and "Postmark", so you can select, which kind you want to use. Best regards Jürgen
  22. Hello @Juergen – Thank you for developing this great module. It looks like the outgoing emails from this module cannot use WireMail() custom modules though. It is possible to make it work with custom WireMail module. I am specifically using WireMailPostmark for all outgoing emails. Thank you!
  23. Hi all - I recently started getting the GET_LOCK error on an old client's website for the API calls that are being made. The VPS web and MySQL servers are separate (have been for a while) and are hosted on DreamHost. I just can't seem to get a handle on it. I tried to uninstall SessionHandlerDB and that led to more problems so I reverted the change. I have SessionHandlerDB v0.0.6 and PW v3.0.227. When I uninstalled the module, I immediately got an error that said : Compile Error: Declaration of WireMailSmtp::attachment($filename) must be compatible with WireMail::attachment($value, $filename = ") (line 550 of site/modules/WireMailSmtp/WireMailSmtp.module This error message was shown because: you are logged in as a Superuser. Administrator has been notified. Error has been logged. Note that I will see that error every now and then now too when I go to the modules pages in the admin portal. After I uninstalled it, I reverted by restoring the "modules" and "sessions" database tables to get things back up to a state where the site loads again. There was a beta version of the module that @netcarverhad posted in another forum thread. I tried that too, but to no avail. Any ideas? Things to check? Thanks for any help and tips!
  24. Lots of people have been asking for a way for ProcessWire to support sending of email, outside of just using PHP's mail() function. I haven't really wanted to expand the scope of ProcessWire that deep into email sending, but I have been wanting to setup a way so that people could override how emails are sent, with modules. For people that are interested in making other ways of sending email in ProcessWire, I've setup a new module base class called WireMail, and a new function called wireMail(). The wireMail() function will use whatever WireMail module is installed. If none is installed, then it will use PW's default WireMail implementation (based on PHP's default mail function). The wireMail() function replaces all instances of PHP's mail() function in ProcessWire's source. It works in a similar way as PHP's mail() except that supports a few different usages. Standard usage would be this: // to, from, subject, body wireMail('user@domain.com', 'ryan@runs.pw', 'Mail Subject', 'Mail Body'); Another usage would be to give it no arguments, and it'll return whatever WireMail module is installed for you to use yourself. If no WireMail module is installed, then it returns ProcessWire's WireMail. $mail = wireMail(); $mail->to('user@hi.com')->from('ryan@runs.pw'); // all calls can be chained $mail->subject('Mail Subject'); $mail->body('Mail Body'); $mail->bodyHTML('<html><body><h1>Mail Body</h1></body></html>'); $mail->send(); Since all of this stuff is only on the PW 2.4 dev branch at present, I'm posting this primarily for people that are interested in creating WireMail modules. For instance, I know that both Teppo and Horst (and perhaps others?) have put together such modules and ideas, so this is all aimed at coming up with a way for those ideas to be easily integrated into PW by way of modules. To make your own WireMail module, you simply create a module that extends WireMail and provide your own implementation for the send() method. Of course, you can go further than that, but that's all that is technically necessary. I've attached an example module called WireMailTest that demonstrates a WireMail module. When installed, it is used rather than PW's WireMail. This WireMailTest module includes lots of comments for you in the code of it, and you may find it helpful to use it as your starting point. WireMailTest.module For you guys developing modules, please throw any questions my way and I'm happy to help. Likewise, let me know if you think I'm missing anything important in the base interface that the modules are based upon and we can update it.
  25. Hi there, in ProcessWire I've installed two modules for sending emails: WireMail SMTP and WireMail Mailgun. I would like to use the WireMail Mailgun for sending an email just for a given case. When I use the following $mailer = WireMail(); $mailer->from('sender@email.com'); $mailer->to('receiver@email.com'); $mailer->subject('Testing Mailgun'); $mailer->body('Just testing.'); $mailer->send(); then the email is sent by the WireMail SMTP module. How can I send the email using WireMail Mailgun?
×
×
  • Create New...