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. 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.
  2. Ever felt like your ProcessWire emails look like they're stuck in 1999? You know the drill - sending emails is super easy with WireMail: $m = new WireMail(); $m->from('foo@bar.com'); $m->to('xxx@yyy.com'); $m->subject('Hello there!'); $m->bodyHTML('<h1>This is great!</h1><p>I am an ugly mail...</p>'); $m->send(); But let's be honest - they look about as pretty as a website built with Microsoft FrontPage! πŸ˜… πŸͺ„ Enter the Mail Pimp Hook! Drop this magical hook into your /site/ready.php (or even better Site.module.php), and watch your emails transform from ugly ducklings into beautiful swans: <?php $wire->addHookBefore('WireMail::send', function(HookEvent $event) { // double check that we got a wiremail instance // this also tells the IDE what $mail is (to get IntelliSense) $mail = $event->object; if (!$mail instanceof WireMail) return; // get current mail body $html = $mail->get('bodyHTML'); if (!$html) return; // get email layout markup $layoutFile = wire()->config->paths->templates . 'mails/default.html'; if (!is_file($layoutFile)) return; // replace ##content## with actual mail content $html = str_replace( '##content##', $html, wire()->files->render($layoutFile) ); // write new body to mail $mail->bodyHTML($html); }); The HTML Just create a beautiful MJML template at /site/templates/mails/default.mjml, put ##content## where your email content should go, convert it to HTML and BOOM! πŸ’₯ Every email gets automatically wrapped in your gorgeous template. No more CSS wrestling matches, no more "Why does this look different in Outlook?" headaches. Just pure email beauty, automagically! ✨ Now your clients will think you spent days crafting those emails, when in reality, you're sipping coffee while your hook does all the heavy lifting. Work smarter, not harder! πŸš€ #ProcessWire #EmailMagic #NoMoreUglyEmails PS: This is the MJML template that I used: <mjml> <mj-head> <mj-attributes> <mj-all font-family="Tahoma" /> <mj-text line-height="140%" /> </mj-attributes> </mj-head> <mj-body background-color="#efefef"> <mj-section background-color="#ffffff" background-repeat="repeat" padding-bottom="30px" padding-top="30px" text-align="center" > <mj-column> <mj-image align="center" padding="25px" src="xxx" target="_blank" width="200px" alt="Logo" ></mj-image> <mj-text>##content##</mj-text> </mj-column> </mj-section> <mj-section> <mj-column> <mj-text font-size="10px" color="#a0a0a0" align="center" > powered by <a href="https://www.baumrock.com/" style="color: #158f66" >baumrock.com</a > </mj-text> </mj-column> </mj-section> </mj-body> </mjml> VSCode has an extension to get a live preview and export MJML to HTML: And here are some other free templates: https://mjml.io/templates I use https://www.base64-image.de/ to add the logo to my mail template as src="data:image/jpeg;base64,/9j/4QAWRXhpZgAATU0AKgAAAA..." to avoid headaches with image paths, remote assets blocking etc.
  3. I have never come across with before β€” especially odd as I use this hosting company for scores of ProcessWire sites but I am having issues with email sending. I am just using normal WireMail() and this works in about 95% of my sites so I don't need to set up a separate SMTP account. The hosting company is claiming in this instance is it because the PW module is not setting the -f $from_address in PHP mail()'s $additional_params paramater. (I am using LoginRegisterPro and the email in question is the confirmation one. I guess this is irrelevant since LRP will use whatever mail module PW is set to use.) Anyway, can I set the -f flag in mail() without hacking the PW core code? Thanks.
  4. Hello at all I encountered a strange issue when sending emails using the Wiremail class from a shared host (world4you.com). Short description of the problem I added the following code from the documentation for the WireMail class to one of my templates for testing purposes: $m = $mail->new(); $m->to('myemailadress@example.at') // will be replaced with my real mail addy ->from('testmail@test.at') ->subject('Message Subject') ->body('Optional message body in plain text') ->bodyHTML('<html><body><p>Optional message body in HTML</p></body></html>') ->send(); After loading a page with this template, the email should be sent to my email address, but I never receive an email. If I use the same code on my local Installation with XAMPP using Gmail as my mail Client everything works as expected. So I tried to figure out where the problem might be, and I discovered the following: The WireMail class contains the send() method, which is responsible for sending the email. There you will find the PHP mail function, which contains a header variable ($header). If I remove this header variable from the brackets, the email is sent; otherwise, it is not. But the email content will not be displayed properly without this header variable. Before removing the header variable: if(@mail($to, $subject, $body, $header)) $numSent++; After: if(@mail($to, $subject, $body)) $numSent++; The problem could therefore be due to an incorrect header. So I took a look at the function renderMailHeader() which is responsible for creating the mail header syntax. I found out that the email is only sent if the value of β€œFrom” in the header is enclosed in double quotation marks, otherwise it will not be sent (see figure below). This does not seem to be important on my local installation. By default, the "From" value (in this case "myemail@example.com) is not between quotes in the header. If I change this inside the renderMailHeader() function, the mail will be sent. Only to mention: There is no entry in the log files indicating that something went wrong. Does anyone else have the same problem on a shared host and know how to fix it? I don't want to use SMTP instead, though. I guess there must be a better solution than changing something in the code or the problem is not the header syntax. Perhaps the header is not the problem, but rather the problem is related to something else?!? Thanks in advance for your help and hints
  5. Hi everyone, I tried a lot, but I donΒ΄t know if IΒ΄m on the right track. I hope, someone can help. I store orders in pages. Every order has a FileField "invoice_pdf". To prevent public access to the PDF, the page template has a pagefileSecure setting and no viewing rights for guest role. The files are then saved unter /site/assets/files/-XXXX/ -> the - prefix is part of the pagefileSecure setting, I guess. Now I want to send the PDF as mail attachment to my customer. But the PDF->url / PDF->httpURL() contains the path without the - prefix. The mail is sent without attachment. I guess because the real path doesnΒ΄t countain a file. Is it possible, to send attachment-files, uploaded to a pagefileSecure field? Thanks a lot for your help!
  6. Hello @perplexed Are you sure that you have added this method to the WireMail object and not to the form object? I have tested it without problems. Please take a look here at the example of a simple contact form and let me know if this solves your problem. JΓΌrgen
  7. I'm using SMTP successfully with Google and an app password for other clients. A client has a custom stricter setup and they say they can only use SMTP with Oauth2. I presume this will involve regularly generating access tokens. Is there any way to make this work with the WireMail SMTP module? There is nothing in the settings. I need it to work at a global level so all $mail routes through this since I am using the LoginRegisterPro and EmailNewUser modules. Thanks.
  8. Btw, if there are modules or hooks that send only plain text emails like for example some notifications on LoginRegisterPro with the following hook you can force HTML on all outgoing emails and forget about those ugly emails 😎 /** * Force HTML on all outgoing emails * */ $wire->addHookBefore('WireMail::send', function(HookEvent $event) { $wireMail = $event->object; if($wireMail->body && !$wireMail->bodyHTML) { $forceHTML = nl2br($wireMail->body); // nl2br β€” Inserts HTML line breaks before all newlines in a string $wireMail->bodyHTML($forceHTML); } }); I guess this can get combined with your hook but for now I'm using it as separate.
  9. 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
  10. Tip: In Symfony Console, if you run code and an Exception or WireException occurs, ProcessWire will not be able to log it (and also send an adminEmail). There were cases where I was calling an undefined method on a page object and not knowing about it erroring. I used AI to work through this and ultimately it said I have to basically manage this myself because Symfony Console will catch things if I don't and never hit ProcessWire. Therefore, you may want to do something like this in your handle method: public function handle() { try { nonExistentFunction(); // purposely cause an error } catch (\Throwable $e) { // log the error $this->wire()->log->save('errors', $e->getMessage()); // consider adding additional code here to send an email notification using WireMail // ... // Re-throw so it still displays in console throw $e; } return self::SUCCESS; } I wonder if there's a more efficient way to do this? It took me too long to figure out what was happening in the first place so I haven't spent time optimizing the approach (if it's even possible).
  11. This module extends WireMail base class, integrating the PHPMailer mailing library into ProcessWire. Module Directory - Githup repo
  12. Hello! I want to find out if this plugin is able to take over all mail sending within ProcessWire automatically by simply replacing the default WireMail Class. If so, where can I configure "SMTP via OAuth" like it is described here: https://github.com/PHPMailer/PHPMailer/wiki/Microsoft-Azure-and-XOAUTH2-setup-guide Is that even possible, or do I have to manually go through all my websites forms and send the mail programmatically? Luckily, I do have all the Azure Application-IDs and secret codes right here, but where do I fill them in?
  13. 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
  14. 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?
  15. No, but I think there must be another cause than I have thought first. The strange thing is that I can send mails by using 2 of my modules with the WireMail class without problems on that host. Only the third module makes problems. I have solved this problem for now by using the WireMailSmtp module.
  16. Do you know how to hook into WireMail so I can set this manually? I thought it was based on Swift but I just read that's discontinued now. I'd be able to get it working with Swift but I need it to use WireMail for LRP, EmailNewUser, etc to work.
  17. 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
  18. 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
  19. If you can, maybe try temporarily disabling Login Register Pro to default to the standard PW login mechanism. Use the PHP code-based password reset trick for a specific account that only you have access to, and see if you're able to successfully login. If you're able to log in, you may need to check the (WireMail) email settings on the new host, as well. If that works, try re-enabling Login Register Pro and see if that now works. If not, you might need to post in the Pro support forum for that module. (I don't use it, so I'm not sure what to suggest.)
  20. 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? ?
  21. Hey @marie.mdna sorry for the delay. I was sick and had to have some days off the computer πŸ™‚ Thx! And thx for your question! The answer to these kind of questions is probably always the same: Yes and no. Yes, you can for sure build such a scenario. No, not out of the box. It's similar as if you asked "does ProcessWire support selling digital goods?" RockCommerce tries to be a good foundation for selling anything online and using ProcessWire to manage all the data. So it will handle the basics for you that are the same for any store that I can think of (meaning handling orders, for example), but everything else usually has to be built by the developer. With product variations being one exception as not every store might need it, but if it does, it would be an absolute nightmare to have to build it on your own. So to answer your question: Just have a look at my website - I'm doing exactly that! You can buy my modules there. Once you buy one, the store sends you an email with the license key. The mail is sent by WireMail, the logic for the license key is built on my own. The invoice is created using RockPdf. The thing is: Every shop is different. Every business is different. How long is a license valid? What does it allow? Etc, etc. I tried to provide a good foundation that saves you a lot of time and makes a lot possible with the least possible effort. Just like ProcessWire does with the $page magic and the admin interface to manage content. Does that make sense? The great thing about RockCommerce is that when using it, you have all your data in ProcessWire. You control that data, you define its structure. And you can easily add your own business logic on top of it - exactly like you want. So many things are just some lines of ProcessWire API away πŸ™‚ But that also means that you might have to build that logic on your own, that other shops might already have built in. In the way they think is best, of course πŸ˜‰ Which might fit your needs or not... I have added this to the docs: https://www.baumrock.com/en/processwire/modules/rockcommerce/docs/checkout/
  22. 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
  23. 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!
  24. Hey @Cybermano glad it was helpful! That's what I've been using before I had the idea of hooking into WireMail::send. Everything in life has pro's and con's... So I'd not say it's a bad practise. But what I like about the solution is that it fits my credo "simple things should be simple". Sending a beautiful thing should be simple. And what you show might look QUITE simple it's not as simple as my version. If you only have one mail it won't matter or your version would even be easier (because you see everything that is going on). But if you are sending mails from different places in different occasions (eg on registration, on a new post, on a schedule, etc...) then my version keeps your code DRY (don't repeat yourself) whereas your version either tempts the dev to copy and paste those lines of code or be too lazy and just send out an ugly 90s style email. Both is not ideal in my opinion and investing a little more time to setup the hook will be beneficial in the long run. Also, while it's just a few lines of code, when your codebase grows every line of code that is not instantly and easily understandable matters. Your brain has to read the code, understand it, interpret it and remember it. With your str_replace that might be QUITE easy, but things add up and suddenly it might make the difference between easy to read code and a bloat of spaghetti code. It's about building a habit. Another problem with your solution is that if you copy&paste this to 3 spots, for example, and later you add another replacement tag, you have to do a search&replace and you might miss one spot and you introduce a bug. If you have it on a central place this can not happen. On the other hand if you want to render different files or you want different tags for different emails your approach might be the better one. And for someone not familiar with your codebase it might be easier to understand your version vs. the hook, because only seeing the $mail->send() somewhat hides how the mail gets sent with nice HTML and your version makes that obvious.
  25. Support thread for WireMail Mailgun. For bug reports, please use the bug tracker on GitHub. https://github.com/plauclair/WireMailMailgun
Γ—
Γ—
  • Create New...