├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SimpleContactForm.module ├── SimpleContactFormConfig.php ├── doc ├── overwrite-classes-and-markup.md ├── spam-translate.md └── success-message.md ├── lib ├── Mailer.php └── SpamProtection.php └── resources ├── contact.js └── jquery.simplecontactform.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 1.0.8 (2018-01-07) 4 | 5 | - add option to redirect to same page to prevent form resubmission 6 | 7 | ### 1.0.7 (2017-11-29) 8 | 9 | - exclude email and password confirmation fields from spam count 10 | 11 | ### 1.0.6 (2017-11-01) 12 | 13 | - add possibility to specify a template in which the form data should be saved 14 | 15 | ### 1.0.5 (2017-09-26) 16 | 17 | - exclude checkbox fields (if unchecked) from spam count 18 | - because it's a standard browser behaviour that the value of a checkbox is only sent if the checkbox was checked 19 | - this leads to a mismatch while counting fields 20 | - the number of submitted fields does not match the number of fields which are present in the form 21 | 22 | ### 1.0.4 (2017-09-14) 23 | 24 | - extend option `classes` 25 | - allows customization of classes for form error and success 26 | 27 | ### 1.0.3 (2017-05-14) 28 | 29 | - corrects typo in success message: **The translation string has been changed, some may need to translate it again!** // thanks @szabesz 30 | - hides date field label // thanks @binarious 31 | - makes send btn text translatable 32 | 33 | ### 1.0.2 (2017-03-18) 34 | 35 | - adds option to specify a redirect page 36 | 37 | ### 1.0.1 (2016-12-16) 38 | 39 | - adds setting `sendEmails`, define whether Emails should be sent 40 | 41 | ### 1.0.0 (2016-03-23) 42 | 43 | - adds ProcessWire 3.x compatibility. Choose branch `2.x` if you want to use it with a version below 3.x 44 | - outsources Mailer and SpamProtection 45 | - adds namespaces support 46 | - adds a Reply-To-Header (optional) 47 | 48 | ### 0.2.1 (2016-01-10) 49 | 50 | - data will be stored, regardless whether a mail has been sent or not 51 | - save corresponding log entry 52 | 53 | ### 0.2.0 (2016-01-06) 54 | 55 | - supports multiple instances 56 | - adds usage of full ProcessWire API in options 57 | - allows to render more than one contact form on a page 58 | - allows to send more than one email 59 | - makes validation hookable 60 | 61 | ### 0.1.2 (2015-08-17) 62 | 63 | - fixes CSRF Token validation 64 | - allows to overwrite mail template 65 | - allows multiple email recipients 66 | 67 | ### 0.1.1 (2015-04-09) 68 | 69 | - updates template handling 70 | 71 | ### 0.1.0 (2015-03-24) 72 | 73 | - additional spam protection and logging 74 | 75 | ### 0.0.9 (2015-03-09) 76 | 77 | - little bugfixes 78 | 79 | ### 0.0.8 (2015-01-22) 80 | 81 | - uses `wireMail` instead of php mail function 82 | 83 | ### 0.0.7 (2014-11-18) 84 | 85 | - adding spam protection comparing post fields 86 | 87 | ### 0.0.6 (2014-11-11) 88 | 89 | - adds php template support 90 | 91 | ### 0.0.5 (2014-10-05) 92 | 93 | - extends anti spam functionality 94 | 95 | ### 0.0.4 (2014-09-30) 96 | 97 | - adds spam protection, honeypot as well as timestamp comparison 98 | 99 | ### 0.0.3 (2014-08-18) 100 | 101 | - save received messages 102 | 103 | ### 0.0.1 (2014-07-11) 104 | 105 | - initial module 106 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING: This repository is no longer maintained :warning: 2 | 3 | > This repository will not be updated. The repository will be kept available in read-only mode. 4 | 5 | # SimpleContactForm for ProcessWire 6 | 7 | Compatibility: ProcessWire 3.x 8 | 9 | Just a simple contact form including spam protection. 10 | See [ProcessWire Forums - Support Board](https://processwire.com/talk/topic/8254-simple-contact-form-optional-twig-support/). 11 | 12 | **Too long to read:** 13 | Install the module, fill in module settings, execute the module: `echo $modules->get('SimpleContactForm')->render();`. 14 | 15 | ### FAQ 16 | 17 | - [How to overwrite classes and markup][1] 18 | - [How to translate the spam message][2] 19 | - [How to add a custom success message][3] 20 | 21 | ## Module Settings 22 | 23 | Fill in module settings, add all fields you want to attach to the form. You could either use existing fields or create new ones. 24 | All new fields will be prefixed with `scf_`. 25 | 26 | If you want to change the field settings, edit the field and change all settings there (e.g. fieldtype, required, length). 27 | 28 | ## Basic Usage 29 | 30 | In the template add just one line to include the form: 31 | 32 | ```php 33 | echo $modules->get('SimpleContactForm')->render(); 34 | ``` 35 | 36 | If you want to send the form via ajax, include _/site/modules/SimpleContactForm/resources/jquery.simplecontactform.js_ and call it: 37 | 38 | ```javascript 39 | if ($('.js-simplecontactform').length) { 40 | $.simplecontactform($('.js-simplecontactform')); 41 | } 42 | ``` 43 | 44 | To get just the necessary part, modify your template like this: 45 | 46 | ```php 47 | ajax) { 49 | $modules->get('SimpleContactForm')->render(); 50 | } else { 51 | // html, header, nav etc. 52 | $modules->get('SimpleContactForm')->render(); 53 | // html, footer etc. 54 | } 55 | ``` 56 | 57 | ## Spam Protection: Hide honeypot field using CSS 58 | 59 | Spam bots fill in automatically all form fields. By adding an invisible field you're able to trick the bots. The key to the honeypot technique is that the form only can be sent when the honeypot field remains empty otherwise it will be treated as spam. 60 | 61 | The honeypot technique doesn't interfere with the user experience. It demands nothing extra of them like a captcha does. In fact, user won't even notice you're using it. 62 | 63 | All that's required is a visually hidden form field. This form adds such a field named `scf-website` by default but you have to make sure to add a **display: none;** CSS rule on it. 64 | 65 | ## Example Email Message 66 | 67 | ``` 68 | Hi! 69 | 70 | You have received a new message: 71 | 72 | Name: %fullName%, 73 | Mail: %email%, 74 | Phone Number: %phone%, 75 | Message: %message%, 76 | 77 | Date: %date% 78 | 79 | ByeBye 80 | ``` 81 | 82 | ## Logging 83 | 84 | This module creates a log file named **simplecontactform-log.txt**. 85 | 86 | > Admin > Setup > Logs > Simplecontactform-log 87 | 88 | In this file every action is logged and marked with ```[SUCCESS]``` or ```[FAILURE]``` 89 | 90 | If a message is treated as spam an entry in the spam log file will be added containing the reason why. 91 | 92 | 2016-02-09 11:02:15 [SUCCESS] Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36, 127.0.0.1, email@test.com 93 | 2016-03-23 10:55:05 [FAILURE] This IP address was already marked as spam. IP: 127.0.0.1 94 | 2016-03-21 12:35:05 [FAILURE] This IP address submitted this form too often. IP: 111.0.5.1 95 | 96 | ## Mark messages as spam 97 | 98 | If the save messages setting is turned on you have the possibility to mark received messages as spam by adding the IP address to a blacklist. 99 | 100 | To mark a message as spam edit the belonging page. Each page has a checkbox to matk the IP address as spam. 101 | 102 | ## How to translate 103 | 104 | All phrases like email subjects are translatable. 105 | Add the module file to the language translator and start translating. 106 | Phrases which don't exist in this file belong to the ProcessWire core. 107 | For example the messages *Please enter a valid e-mail address* or *Missing required value*. 108 | 109 | Relevant Files: 110 | 111 | - site/modules/SimpleContactForm/lib/SpamProtection.php 112 | - wire/modules/Inputfield/InputfieldEmail.module 113 | - wire/core/InputfieldWrapper.php 114 | 115 | Depending on the fields you added to the form there might be some other files. 116 | 117 | ## Render multiple instances 118 | 119 | You can pass options as an array which overwrites the defaults set up in module settings. 120 | You don't have to pass all available keys, if you skip one, the input from module settings will be used (for example emailServer stays the same). 121 | 122 | Here is an example: 123 | 124 | ```php 125 | $scf = $modules->get('SimpleContactForm'); 126 | 127 | $options = array( 128 | 'emailSubject' => 'Test Subject', 129 | 'emailAdd' => true, 130 | 'emailAddSubject' => 'hi there', 131 | 'emailAddMessage' => 'Hi %scf_fullName%', 132 | 'emailAddReplyTo' => $input->scf_email, 133 | 'emailAddTo' => $input->scf_email 134 | ); 135 | 136 | echo $scf->render($options); 137 | ``` 138 | 139 | ### Available Keys 140 | 141 | | key | type | description | 142 | |----------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------- | 143 | | allFields | string | comma-separated list of all fields | 144 | | submitName | string | if you use more than one form at one page, you have to pass the submit button name. That means, you have to use different submit names | 145 | | btnClass | string | add custom submit button class(es), defaults to `button` | 146 | | btnText | string | add custom submit button text, defaults to `Send` | 147 | | action | string | set specific form action, defaults to same page './' | 148 | | sendEmails | boolean | whether Emails should be sent | 149 | | redirectSamePage | boolean | Redirect to the same page after successfull submission to prevent form resubmission. OPTIONAL. | 150 | | redirectPage | integer | Redirect to a specific page after successfull submission. OPTIONAL: If you prefer to stay on the same page, just leave this field empty.| 151 | | emailMessage | string | email message | 152 | | successMessage | string | success message | 153 | | errorMessage | string | error message | 154 | | emailAddMessage | string | email message | 155 | | emailSubject | string | email subject | 156 | | emailTo | string | email address of recipient | 157 | | emailServer | string | server address | 158 | | emailAdd | boolean | set this to true if you want to send more than one email | 159 | | emailAddSubject | string | email subject | 160 | | emailAddTo | string | email to | 161 | | emailAddReplyTo | string | email reply to | 162 | | saveMessages | boolean | whether to save received messages | 163 | | saveMessagesParent | integer | All items created and managed will live under the parent you select here | 164 | | saveMessagesTemplate | integer | Template for received messages | 165 | | markup | array | overwrite markup | 166 | | classes | array | overwrite classes | 167 | | prependMarkup | string | prepend some markup/content | 168 | | appendMarkup | string | append some markup/content | 169 | 170 | To get an overview of what's possible, have a look at [How to overwrite classes and markup][1] 171 | 172 | ### More options for email templates, emailTo, .. 173 | 174 | You have the full ProcessWire API available. Feel free to add any value you want to! 175 | 176 | ```php 177 | $options['emailTo'] = $input->email; 178 | $options['emailSubject'] = $page->title; 179 | $options['emailMessage'] = $message; 180 | ``` 181 | 182 | `$message` could be a single partial including the whole message. 183 | 184 | ### More than one contact form on a page 185 | 186 | If you need more than one contact form on a single page you have to pass the `$options['submitName']` option. 187 | That means, you have to create a unique name for each submit button. 188 | 189 | ### Send more than one mail 190 | 191 | Sometimes you need to send different emails to different people. 192 | Here is an example how this works: 193 | 194 | ```php 195 | $options['emailAdd'] = true; 196 | $options['emailAddSubject'] = 'hi there'; 197 | $options['emailAddMessage'] = $anotherMessage; 198 | $options['emailAddTo'] = $input->email_recommend; 199 | ``` 200 | 201 | It's important to set `$options['emailAdd] = true;`. Otherwise it won't send an additional email. 202 | If you want to use the same email subject (for example), you can skip this part and it will use the default value. 203 | 204 | ## Add custom validation 205 | 206 | You could add additional custom validation after the form was processed. This allows custom/extra validation and field manipulation. 207 | 208 | ```php 209 | $this->addHookBefore('SimpleContactForm::processValidation', function(HookEvent $event) { 210 | $form = $event->arguments(0); 211 | $email = $form->get('scf_email'); 212 | 213 | // add error if email address already exists 214 | if (count($this->users->find("email={$email->value}")) > 0) { // attach an error to the field 215 | $email->error(__('This email address is already registered.')); // it will be displayed along the field 216 | } 217 | }); 218 | ``` 219 | 220 | [1]: https://github.com/justb3a/processwire-simplecontactform/blob/master/doc/overwrite-classes-and-markup.md 'How to overwrite classes and markup' 221 | [2]: https://github.com/justb3a/processwire-simplecontactform/blob/master/doc/spam-translate.md 'How to translate the spam message' 222 | [3]: https://github.com/justb3a/processwire-simplecontactform/blob/master/doc/success-message.md 'How to add a custom success message' 223 | -------------------------------------------------------------------------------- /SimpleContactForm.module: -------------------------------------------------------------------------------- 1 | 13 | * @version 1.0.7 14 | * @copyright Copyright (c) 2017 15 | * @see https://github.com/justonestep/processwire-simplecontactform 16 | * @see http://www.processwire.com 17 | */ 18 | 19 | /** 20 | * Class SimpleContactForm 21 | */ 22 | class SimpleContactForm extends WireData implements Module { 23 | 24 | /** 25 | * Get module information 26 | * 27 | * @return array 28 | */ 29 | public static function getModuleInfo() { 30 | return array( 31 | 'title' => 'Simple Contact Form', 32 | 'summary' => 'Just a simple contact form.', 33 | 'version' => 107, 34 | 'href' => 'https://github.com/justonestep/processwire-simplecontactform', 35 | 'singular' => true, 36 | 'autoload' => true, 37 | 'icon' => 'envelope', 38 | ); 39 | } 40 | 41 | /** 42 | * string class name 43 | */ 44 | const CLASS_NAME = 'SimpleContactForm'; 45 | 46 | /** 47 | * string template name 48 | */ 49 | const TEMPLATE_NAME = 'simple_contact_form'; 50 | 51 | /** 52 | * string template name for save messages 53 | */ 54 | const SM_TEMPLATE_NAME = 'simple_contact_form_messages'; 55 | 56 | /** 57 | * string tag name 58 | */ 59 | const TAG_NAME = 'scf'; 60 | 61 | /** 62 | * Additional fields 63 | */ 64 | static protected $additionalFields = array('date', 'ip'); 65 | 66 | /** 67 | * Spam protection fields 68 | */ 69 | static protected $spamFields = array('scf-date', 'scf-website', 'sumbit', 'token'); 70 | 71 | /** 72 | * Markup used during the render() method 73 | */ 74 | static protected $markup = array( 75 | 'list' => "{out}\n", 76 | 'item' => "\n\t
\n{out}\n\t
", 77 | 'item_label' => "\n\t\t", 78 | 'item_label_hidden' => "\n\t\t", 79 | 'item_content' => "{out}", 80 | 'item_error' => "\n

{out}

", 81 | 'item_description' => "\n

{out}

", 82 | 'item_head' => "\n

{out}

", 83 | 'item_notes' => "\n

{out}

", 84 | 'item_icon' => "", 85 | 'item_toggle' => "", 86 | // ALSO: 87 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 88 | ); 89 | 90 | /** 91 | * Classes used during the render() method 92 | */ 93 | static protected $classes = array( 94 | 'form' => 'form js-simplecontactform', // additional clases for inputfieldform (optional) 95 | 'form_error' => 'form--error--message', 96 | 'form_success' => 'form--success--message', 97 | 'list' => 'fields', 98 | 'list_clearfix' => 'clearfix', 99 | 'item' => 'form__item form__item--{name}', 100 | 'item_label' => '', // additional classes for inputfieldheader (optional) 101 | 'item_content' => '', // additional classes for inputfieldcontent (optional) 102 | 'item_required' => 'field--required', // class is for inputfield 103 | 'item_error' => 'field--error', // note: not the same as markup[item_error], class is for inputfield 104 | 'item_collapsed' => 'field--collapsed', 105 | 'item_column_width' => 'field__column', 106 | 'item_column_width_first' => 'field__column--first', 107 | 'item_show_if' => 'field--show-if', 108 | 'item_required_if' => 'field--required-if' 109 | // ALSO: 110 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 111 | ); 112 | 113 | /** 114 | * construct 115 | */ 116 | public function __construct() { 117 | // require spam lib 118 | require_once($this->config->paths->SimpleContactForm . 'lib/SpamProtection.php'); 119 | require_once($this->config->paths->SimpleContactForm . 'lib/Mailer.php'); 120 | 121 | // add log file 122 | $this->log = new FileLog($this->config->paths->logs . strtolower(self::CLASS_NAME) . '-log.txt'); 123 | } 124 | 125 | /** 126 | * Initialize the module 127 | * 128 | * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called 129 | * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 130 | */ 131 | public function init() { 132 | $allFieldsExtended = $this->allFields; 133 | foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f; 134 | $this->allFieldsExtended = $allFieldsExtended; 135 | $this->submitName = 'submit'; 136 | $this->btnClass = 'button'; 137 | $this->btnText = $this->_('Send'); 138 | } 139 | 140 | /** 141 | * Initialize the module - ready 142 | * 143 | * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called 144 | * when ProcessWire's API is ready. As a result, this is a good place to attach hooks. 145 | */ 146 | public function ready() { 147 | $this->errorMessage = $this->_('Please verify the data you have entered.'); 148 | $this->successMessage = $this->_('Your contact request has been sent successfully!'); 149 | 150 | $this->addHookBefore('Modules::saveModuleConfigData', $this, 'createAndAddFieldsOnSaveModuleConfigData'); 151 | } 152 | 153 | /** 154 | * Set markup 155 | * 156 | * @param Form $form 157 | * @param array $options 158 | */ 159 | private function setMarkup(&$form, $options) { 160 | $markup = isset($options['markup']) ? array_merge(self::$markup, $options['markup']) : self::$markup; 161 | $form->setMarkup($markup); 162 | } 163 | 164 | /** 165 | * Set classes 166 | * 167 | * @param Form $form 168 | * @param array $options 169 | */ 170 | private function setClasses(&$form, $options) { 171 | $classes = isset($options['classes']) ? array_merge(self::$classes, $options['classes']) : self::$classes; 172 | $form->setClasses($classes); 173 | } 174 | 175 | /** 176 | * Render Success Message 177 | * 178 | * @param Form $form 179 | * @return string 180 | */ 181 | private function renderSuccessMessage($form) { 182 | return "

{$this->successMessage}

"; 183 | } 184 | 185 | /** 186 | * Render Form 187 | * 188 | * @param array $options 189 | * @return string 190 | */ 191 | private function renderForm($options) { 192 | $form = $this->getForm($options); 193 | $this->setMarkup($form, $options); 194 | $this->setClasses($form, $options); 195 | 196 | // on form submit 197 | if ($this->input->post->{$this->submitName} && !$this->input->get->success) { 198 | // process form input and validate fields 199 | $form->processInput($this->input->post); 200 | $this->processValidation($form); 201 | 202 | // anti spam measures 203 | if (!$form->getErrors()) { 204 | $spamProtector = new SpamProtection(); 205 | 206 | // exclude markup fields from spam count 207 | // AND exclude checkbox fields (if unchecked) from spam count 208 | // AND exclude password confirmation field from spam count BUT the other way 209 | // AND exclude verify email BUT the other way 210 | // BECAUSE it's a standard browser behaviour that the value of a checkbox is only sent if the checkbox was checked 211 | $excludeFields = 0; 212 | foreach ($this->allFields as $inputfield) { 213 | if ($field = $this->fields->get($inputfield)) { 214 | if ($field->type instanceof FieldtypeFieldsetOpen || $field->type instanceof FieldtypeFieldsetTabOpen) $excludeFields++; 215 | if ($field->type instanceof FieldtypeCheckbox && !$this->input->post->{$field->name}) $excludeFields++; 216 | if ($field->type instanceof FieldtypePassword) $excludeFields--; 217 | if ($field->type instanceof FieldtypeEmail && $field->confirm) $excludeFields--; 218 | } 219 | } 220 | 221 | $spamProtector 222 | ->setCount(count($this->allFields) - $excludeFields + count(self::$spamFields)) 223 | ->setTimeRange($this->antiSpamTimeMin, $this->antiSpamTimeMax) 224 | ->setSaveMessages($this->saveMessages) 225 | ->setExcludeIpAdresses($this->antiSpamExcludeIps) 226 | ->setNumberOfSubmitsPerDay($this->antiSpamPerDay) 227 | ->validate(); 228 | 229 | // didn't pass spam test 230 | if ($spamProtector->isSpam()) { 231 | $form->error(sprintf( 232 | $this->_("Sorry, but your message didn't pass our %s test. Please prepare another %s."), 233 | $spamProtector->getAnimal(), 234 | $spamProtector->getFruit() 235 | )); 236 | $form->appendMarkup .= "

{$form->getErrors()[0]}

"; 237 | $d = $form->get('scf-date'); 238 | $d->attr('value', time()); 239 | } 240 | } else { 241 | $form->appendMarkup .= "

{$this->errorMessage}

"; 242 | } 243 | 244 | // send mail 245 | if (!$form->getErrors()) { 246 | if ($this->sendEmails) $this->sendMail(); 247 | if ($this->saveMessages) $this->saveMessage(); 248 | 249 | if ($this->redirectSamePage) { 250 | $this->session->redirect($this->page->url . '?success=1'); 251 | } elseif ($p = $this->redirectPage) { 252 | $this->session->redirect($this->pages->get($p)->url); 253 | } else { 254 | $out = $this->renderSuccessMessage($form); 255 | } 256 | } 257 | } 258 | 259 | if ($this->input->get->success) $out = $this->renderSuccessMessage($form); 260 | 261 | return isset($out) ? $out : $form->render(); 262 | } 263 | 264 | /** 265 | * Get form 266 | * 267 | * @param array $options 268 | * @return Form 269 | */ 270 | private function getForm($options) { 271 | $form = $this->modules->get('InputfieldForm'); 272 | 273 | $form->action = isset($options['action']) ? $options['action'] : './'; 274 | $form->method = 'post'; 275 | $form->attr('id+name','contact-form'); 276 | if (isset($options['prependMarkup'])) $form->prependMarkup = $options['prependMarkup']; 277 | if (isset($options['appendMarkup'])) $form->appendMarkup = $options['appendMarkup']; 278 | 279 | // add fields 280 | if (is_array($this->allFields)) { 281 | foreach ($this->allFields as $fieldname) { 282 | if ($field = $this->fields->get($fieldname)) { 283 | $inputfield = $field->getInputfield($this->page); 284 | $inputfield->useLanguages = false; 285 | $form->append($inputfield); 286 | } 287 | } 288 | } 289 | 290 | // add honeypot (spam protection) 291 | $honeyField = $this->modules->get('InputfieldText'); 292 | $honeyField->name = 'scf-website'; 293 | $honeyField->initValue = ''; 294 | $form->append($honeyField); 295 | 296 | // add hidden field to save current timestamp 297 | $field = $this->modules->get('InputfieldHidden'); 298 | $field->skipLabel = Inputfield::skipLabelBlank; 299 | $field->attr('name+id', 'scf-date'); 300 | $field->attr('value', time()); 301 | $field->required = 1; 302 | $form->append($field); 303 | 304 | // add a submit button to the form 305 | $submit = $this->modules->get('InputfieldSubmit'); 306 | $submit->name = $this->submitName; 307 | $submit->attr('class', $this->btnClass); 308 | $submit->attr('value', $this->btnText === 'Send' ? $this->_('Send') : $this->btnText); 309 | $form->append($submit); 310 | 311 | return $form; 312 | } 313 | 314 | /** 315 | * Render 316 | * available keys: 317 | * allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText 318 | * emailMessage, emailAddMessage, successMessage, errorMessage 319 | * sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo 320 | * saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup 321 | * 322 | * @param array $options 323 | * @return string 324 | */ 325 | public function ___render($options = array()) { 326 | return $this->renderInstance($options); 327 | } 328 | 329 | /** 330 | * Render further instance 331 | * available keys: 332 | * allFields, redirectSamePage, redirectPage, submitName, actionPage, btnClass, btnText 333 | * emailMessage, emailAddMessage, successMessage, errorMessage 334 | * sendEmails, emailSubject, emailTo, emailServer, emailReplyTo, emailAdd, emailAddSubject, emailAddTo, emailAddReplyTo 335 | * saveMessages, saveMessagesParent, saveMessagesTemplate, markup, classes, prependMarkup, appendMarkup 336 | * 337 | * @param array $options 338 | * @return string 339 | */ 340 | private function renderInstance($options) { 341 | // overwrite module config settings 342 | foreach ($options as $key => $value) { 343 | switch ($key) { 344 | // === fields 345 | case 'allFields': 346 | $this->allFields = explode(',', $value); 347 | $allFieldsExtended = explode(',', $value); 348 | foreach (self::$additionalFields as $f) $allFieldsExtended[] = $f; 349 | $this->allFieldsExtended = $allFieldsExtended; 350 | break; 351 | 352 | case 'redirectSamePage': 353 | $this->redirectSamePage = $value; 354 | break; 355 | 356 | case 'redirectPage': 357 | $this->redirectPage = $value; 358 | break; 359 | 360 | // === names 361 | case 'submitName': 362 | $this->submitName = $value; 363 | break; 364 | 365 | case 'btnClass': 366 | $this->btnClass = $value; 367 | break; 368 | 369 | case 'btnText': 370 | $this->btnText = $value; 371 | break; 372 | 373 | // === messages 374 | case 'successMessage': 375 | $this->successMessage = $value; 376 | break; 377 | case 'errorMessage': 378 | $this->errorMessage = $value; 379 | break; 380 | case 'emailMessage': 381 | $this->emailMessage = $value; 382 | break; 383 | case 'emailAddMessage': 384 | $this->emailAddMessage = $value; 385 | break; 386 | 387 | // === email 388 | case 'sendEmails': 389 | $this->sendEmails = $value; 390 | break; 391 | case 'emailSubject': 392 | $this->emailSubject = $value; 393 | break; 394 | case 'emailTo': 395 | $this->emailTo = $value; 396 | break; 397 | case 'emailServer': 398 | $this->emailServer = $value; 399 | break; 400 | case 'emailReplyTo': 401 | $this->emailReplyTo = $value; 402 | break; 403 | case 'emailAdd': 404 | $this->emailAdd = $value; 405 | break; 406 | case 'emailAddSubject': 407 | $this->emailAddSubject = $value; 408 | break; 409 | case 'emailAddTo': 410 | $this->emailAddTo = $value; 411 | break; 412 | case 'emailAddReplyTo': 413 | $this->emailAddReplyTo = $value; 414 | break; 415 | 416 | // === general 417 | case 'saveMessages': 418 | $this->saveMessages = $this->boolval($value); 419 | break; 420 | 421 | case 'saveMessagesParent': 422 | $this->saveMessagesParent = $value; 423 | break; 424 | 425 | case 'saveMessagesTemplate': 426 | $this->saveMessagesTemplate = $value; 427 | break; 428 | } 429 | } 430 | 431 | // send additional email? 432 | if (isset($this->emailAdd) && $this->emailAdd) { 433 | if (!isset($this->emailAddMessage)) $this->emailAddMessage = $this->emailMessage; 434 | if (!isset($this->emailAddTo)) $this->emailAddTo = $this->emailTo; 435 | if (!isset($this->emailAddReplyTo)) $this->emailAddReplyTo = $this->emailReplyTo; 436 | if (!isset($this->emailAddSubject)) $this->emailAddSubject = $this->emailSubject; 437 | } 438 | 439 | // execute render 440 | return $this->renderForm($options); 441 | } 442 | 443 | /** 444 | * Get mail message content 445 | * 446 | * @param string $text 447 | * @return string 448 | */ 449 | private function getMessageContent($text) { 450 | if (!empty($text)) { 451 | $date = new \DateTime(); 452 | if (preg_match('/\%date\%/', $text)) $text = str_replace('%date%', $date->format('Y-m-d H:i:s'), $text); 453 | preg_match_all('/\%(.*?)\%/', $text, $matches); 454 | 455 | foreach ($matches[0] as $key => $match) { 456 | $text = str_replace($match, $this->sanitizer->textarea($this->input->post->{$matches[1][$key]}), $text); 457 | } 458 | } else { 459 | $message = array(); 460 | foreach ($this->allFields as $inputfield) { 461 | $message[] = $inputfield . ': ' . $this->sanitizer->textarea($this->input->post->{$inputfield}); 462 | } 463 | $date = new \DateTime(); 464 | $message[] = 'Date: ' . $date->format('Y-m-d H:i:s'); 465 | $text = implode("\r\n", $message); 466 | } 467 | 468 | return $text; 469 | } 470 | 471 | /** 472 | * Send Mail 473 | */ 474 | public function ___sendMail() { 475 | $mail = new Mailer( 476 | $this->emailTo, 477 | $this->emailServer, 478 | $this->emailReplyTo, 479 | $this->emailSubject, 480 | trim($this->getMessageContent($this->emailMessage)) 481 | ); 482 | 483 | $numSent = $mail->send(); 484 | 485 | // send additional mail 486 | if ($this->sendEmails && isset($this->emailAdd) && $this->emailAdd) $this->sendAdditionalMail(); 487 | 488 | // log whether a mail has been sent or not 489 | if ($numSent) { 490 | $logmessage = array( 491 | $_SERVER['HTTP_USER_AGENT'], 492 | $_SERVER['REMOTE_ADDR'], 493 | $this->emailTo 494 | ); 495 | 496 | $this->log->save('[SUCCESS] ' . implode(', ', $logmessage)); 497 | } else { 498 | // mail has not been sent 499 | $this->log->save("[ERROR] Mail has not been sent to {$this->emailTo}"); 500 | } 501 | } 502 | 503 | /** 504 | * Save Message 505 | */ 506 | public function ___saveMessage() { 507 | $date = new \DateTime(); 508 | 509 | $name = array($date->getTimestamp()); 510 | if ($parts = $this->saveMessagesScheme) { 511 | foreach ($parts as $part) { 512 | $name[] = $this->input->post->$part; 513 | } 514 | } 515 | $pageName = implode(' ', $name); 516 | 517 | $p = new Page(); 518 | $p->template = $this->saveMessagesTemplate; 519 | $p->parent = wire('pages')->get($this->saveMessagesParent); 520 | $p->name = $this->sanitizer->pageName($pageName, true); 521 | $p->title = $pageName; 522 | $p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below! 523 | 524 | foreach ($this->allFields as $in) $p->$in = $this->input->post->$in; 525 | $p->scf_date = $date->getTimestamp(); 526 | $p->scf_ip = $_SERVER['REMOTE_ADDR']; 527 | $p->save(); 528 | } 529 | 530 | /** 531 | * Send Additional Mail 532 | */ 533 | public function ___sendAdditionalMail() { 534 | $mail = new Mailer( 535 | $this->emailAddTo, 536 | $this->emailServer, 537 | $this->emailAddReplyTo, 538 | $this->emailAddSubject, 539 | trim($this->getMessageContent($this->emailAddMessage)) 540 | ); 541 | 542 | if ($mail->send()) { 543 | $logmessage = array( 544 | $_SERVER['HTTP_USER_AGENT'], 545 | $_SERVER['REMOTE_ADDR'], 546 | $this->emailAddTo 547 | ); 548 | 549 | $this->log->save('[SUCCESS] ' . implode(', ', $logmessage)); 550 | } else { 551 | $this->log->save("[ERROR] Additional mail could not be sent to {$this->emailAddTo}"); 552 | } 553 | } 554 | 555 | /** 556 | * Hook create and add template fields 557 | * 558 | * @param HookEvent $event 559 | */ 560 | public function createAndAddFieldsOnSaveModuleConfigData(HookEvent $event) { 561 | if ($event->arguments[0] === self::CLASS_NAME) { 562 | $configData = $event->arguments[1]; 563 | 564 | // saveMessages enabled? create template if it doesn't exist 565 | $fg = $configData['saveMessages'] ? $this->createSaveMessagesTemplate($configData) : null; 566 | 567 | // get fields 568 | if ($configData['addFields']) { 569 | $this->addNewFields($configData, $fg); 570 | $event->setArgument(1, $configData); 571 | } 572 | 573 | // cleanup, update from 0.x to 1.x @todo: deprecated 574 | $this->upgradeItems($fg, $configData); 575 | } 576 | } 577 | 578 | /** 579 | * Add new fields 580 | * 581 | * @param array $configData 582 | * @param Fieldgroup $fg 583 | */ 584 | protected function addNewFields(&$configData, $fg) { 585 | $newFields = $configData['addFields']; 586 | $allFields = $configData['allFields']; 587 | 588 | foreach (explode(',', preg_replace('/\s/', '', $newFields)) as $name) { 589 | if (is_null($this->fields->get("scf_$name"))) { 590 | $f = new Field(); 591 | $f->type = $this->modules->get('FieldtypeText'); 592 | $f->name = "scf_$name"; 593 | $f->label = 'SCF - ' . ucfirst($name); 594 | $f->tags = self::TAG_NAME; 595 | $f->columnWidth = '25'; 596 | $f->save(); 597 | 598 | // saveMessages enabled - save fields to template 599 | if ($fg) { 600 | $fg->add($f); // add field to fieldgroup 601 | $fg->save(); // save fieldgroup 602 | } 603 | } 604 | 605 | if (!in_array("scf_$name", $allFields)) $allFields[] = "scf_$name"; 606 | } 607 | 608 | $configData['allFields'] = $allFields; 609 | $configData['addFields'] = ''; 610 | } 611 | 612 | /** 613 | * Create save messages template 614 | * 615 | * @param array $configData 616 | * @return Fieldgroup 617 | */ 618 | protected function createSaveMessagesTemplate($configData) { 619 | if ($template = $this->templates->get(self::SM_TEMPLATE_NAME)) { 620 | $fg = $template->fieldgroup; // get existing fieldgroup 621 | } else { 622 | // new fieldgroup 623 | $fg = new Fieldgroup(); 624 | $fg->name = self::SM_TEMPLATE_NAME; 625 | $fg->add($this->fields->get('title')); // needed title field 626 | $fg->save(); 627 | 628 | // new template 629 | $template = new Template(); 630 | $template->name = self::SM_TEMPLATE_NAME; 631 | $template->fieldgroup = $fg; // add the fieldgroup 632 | $template->slashUrls = 1; 633 | $template->noPrependTemplateFile = 1; 634 | $template->noAppendTemplateFile = 1; 635 | $template->tags = self::TAG_NAME; 636 | $template->save(); 637 | } 638 | 639 | // scf_spamip (check) 640 | if (!$fg->scf_spamip) { 641 | if (!$field = $this->fields->get('scf_spamIp')) { 642 | $field = new Field(); 643 | $field->type = $this->modules->get('FieldtypeCheckbox'); 644 | $field->name = 'scf_spamIp'; 645 | $field->value = 1; 646 | } 647 | 648 | $field->label = __('Add IP to spam list'); 649 | $field->description = __('If you activate this checkbox, further contact requests from this ip address will be treated as spam.'); 650 | $field->columnWidth = 25; 651 | $field->save(); 652 | 653 | $fg->add($field); 654 | $fg->save(); 655 | } 656 | 657 | // scf_date (datetime) 658 | if (!$fg->scf_date) { 659 | if (!$field = $this->fields->get('scf_date')) { 660 | $field = new Field(); 661 | $field->name = 'scf_date'; 662 | } 663 | 664 | $field->type = $this->modules->get('FieldtypeDatetime'); 665 | $field->label = __('Creation date'); 666 | $field->datepicker = InputfieldDatetime::datepickerClick; 667 | $field->dateInputFormat = 'Y/m/d'; 668 | $field->timeInputFormat = 'H:i'; 669 | $field->dateOutputFormat = 'Y/m/d'; 670 | $field->timeOutputFormat = 'H:i'; 671 | $field->columnWidth = 25; 672 | $field->save(); 673 | 674 | $fg->add($field); 675 | $fg->save(); 676 | } 677 | 678 | // scf_ip (text) 679 | if (!$fg->scf_ip) { 680 | if (!$field = $this->fields->get('scf_ip')) { 681 | $field = new Field(); 682 | $field->type = $this->modules->get('FieldtypeText'); 683 | $field->name = 'scf_ip'; 684 | } 685 | 686 | $field->label = __('IP address'); 687 | $field->columnWidth = 25; 688 | $field->save(); 689 | 690 | $fg->add($field); 691 | $fg->save(); 692 | } 693 | 694 | $this->validateParent($configData); 695 | return $fg; 696 | } 697 | 698 | /** 699 | * Validate parent page 700 | * 701 | * @param array $configData 702 | */ 703 | protected function validateParent($configData) { 704 | // add default for save messages parent 705 | if (!$configData['saveMessagesParent']) $configData['saveMessagesParent'] = $this->config->rootPageID; 706 | 707 | // check whether selected parent allows children (noChildren must be 0) 708 | if ($this->pages->get($configData['saveMessagesParent'])->template->noChildren > 0) { 709 | $this->log->error($this->_('Please choose another parent or change the belonging template. It must allow children.')); 710 | } 711 | } 712 | 713 | /** 714 | * Validate parent page 715 | * 716 | * @param Fieldgroup $fg 717 | * @param array $configData 718 | */ 719 | protected function upgradeItems($fg, $configData) { 720 | if ($this->fields->get('repeater_scfmessages') && $configData['saveMessages']) { 721 | // first: add fields to template 722 | foreach ($this->allFields as $f) { 723 | $fg->add($f); // add field to fieldgroup 724 | $fg->save(); // save fieldgroup 725 | } 726 | 727 | // second: repater items to pages 728 | foreach ($this->pages->get('template=' . self::SM_TEMPLATE_NAME)->repeater_scfmessages as $item) { 729 | if (!$item->scf_date) continue; 730 | 731 | $name = array($item->created); 732 | if ($parts = $configData['saveMessagesScheme']) { 733 | foreach ($parts as $part) { 734 | $name[] = $item->$part; 735 | } 736 | } 737 | $pageName = implode(' ', $name); 738 | 739 | if ($this->pages->find('name=' . $this->sanitizer->pageName($pageName, true))->count() > 0) continue; 740 | 741 | $p = new Page(); 742 | $p->template = self::SM_TEMPLATE_NAME; 743 | $p->parent = wire('pages')->get($configData['saveMessagesParent']); 744 | $p->name = $this->sanitizer->pageName($pageName, true); 745 | $p->title = $pageName; 746 | $p->save(); // IMPORTANT: Save the page once, so that file-type fields can be added to it below! 747 | 748 | $date = new \DateTime(); 749 | $date->setTimestamp($item->created); 750 | $p->scf_date = $date->format('Y/m/d H:i'); 751 | $p->scf_ip = $item->scf_ip; 752 | foreach ($this->allFields as $f) if ($item->$f) $p->$f = $item->$f; 753 | $p->save(); 754 | } 755 | 756 | // third: delete repeater and template simple_contact_form 757 | $fg->remove($this->fields->get('repeater_scfmessages')); 758 | $fg->save(); 759 | $this->fields->delete($this->fields->get('repeater_scfmessages')); 760 | $this->templates->delete($this->templates->get(self::TEMPLATE_NAME)); 761 | } 762 | } 763 | 764 | /** 765 | * Get the boolean value of a variable 766 | * 767 | * @param $val 768 | */ 769 | public function boolval($val) { 770 | if (!function_exists('boolval')) { 771 | // (PHP 5 < 5.5.0) 772 | $bool = (bool) $val; 773 | } else { 774 | // (PHP 5 >= 5.5.0) 775 | $bool = boolval($val); 776 | } 777 | 778 | return $bool; 779 | } 780 | 781 | /** 782 | * Hookable method called after the form was processed 783 | * Allows custom/extra validation and field manipulation 784 | */ 785 | protected function ___processValidation($form) {} 786 | 787 | } 788 | -------------------------------------------------------------------------------- /SimpleContactFormConfig.php: -------------------------------------------------------------------------------- 1 | templates->get('simple_contact_form_messages'); 13 | 14 | return array( 15 | 'sendEmails' => true, 16 | 'emailTo' => '', 17 | 'emailSubject' => 'New Web Contact Form Submission', 18 | 'emailMessage' => '', 19 | 'emailServer' => 'noreply@server.com', 20 | 'emailReplyTo' => '', 21 | 'allFields' => array(), 22 | 'redirectPage' => '', 23 | 'redirectSamePage' => true, 24 | 'saveMessages' => false, 25 | 'saveMessagesParent' => false, 26 | 'saveMessagesTemplate' => $saveMessagesTemplate ? $saveMessagesTemplate->id : null, 27 | 'saveMessagesScheme' => '', 28 | 'antiSpamTimeMin' => '1', 29 | 'antiSpamTimeMax' => '300', 30 | 'antiSpamPerDay' => '3', 31 | 'antiSpamExcludeIps' => '127.0.0.1', 32 | 'cleanup' => 0 33 | ); 34 | } 35 | 36 | /** 37 | * Retrieves the list of config input fields 38 | * Implementation of the ConfigurableModule interface 39 | * 40 | * @return InputfieldWrapper 41 | */ 42 | public function getInputfields() { 43 | $allFields = isset($this->data['allFields']) ? $this->data['allFields'] : array(); 44 | if (!is_array($allFields)) $allFields = explode(',', $this->data['allFields']); // @todo: deprecated 45 | 46 | // add prefix if necessary 47 | // @todo: deprecated 48 | foreach ($allFields as $key => $f) { 49 | if (!$this->fields->get($f) || $f === 'email') $allFields[$key] = 'scf_' . $f; 50 | } 51 | 52 | // get inputfields 53 | $inputfields = parent::getInputfields(); 54 | 55 | // fieldset general 56 | $fieldset = $this->modules->get('InputfieldFieldset'); 57 | $fieldset->label = __('Email'); 58 | 59 | // field send emails 60 | $field = $this->modules->get('InputfieldCheckbox'); 61 | $field->name = 'sendEmails'; 62 | $field->label = __('Send Emails?'); 63 | $field->description = __('Should Emails be sent?'); 64 | $field->value = 1; 65 | $field->columnWidth = 50; 66 | $fieldset->add($field); 67 | 68 | // field email subject 69 | $field = $this->modules->get('InputfieldText'); 70 | $field->name = 'emailSubject'; 71 | $field->label = __('Email: Subject'); 72 | $field->columnWidth = 50; 73 | $field->required = 1; 74 | $field->requiredIf = 'sendEmails=1'; 75 | $field->showIf = 'sendEmails=1'; 76 | $fieldset->add($field); 77 | 78 | // field email to 79 | $field = $this->modules->get('InputfieldText'); 80 | $field->name = 'emailTo'; 81 | $field->label = __('Email: "To"-Address'); 82 | $field->notes = __('Scheme: "Example , Name "'); 83 | $field->columnWidth = 33; 84 | $field->required = 1; 85 | $field->requiredIf = 'sendEmails=1'; 86 | $field->showIf = 'sendEmails=1'; 87 | $fieldset->add($field); 88 | 89 | // field email server 90 | $field = $this->modules->get('InputfieldText'); 91 | $field->name = 'emailServer'; 92 | $field->label = __('Email: "From"-Address'); 93 | $field->notes = __('Scheme: "FromName "'); 94 | $field->columnWidth = 33; 95 | $field->required = 1; 96 | $field->requiredIf = 'sendEmails=1'; 97 | $field->showIf = 'sendEmails=1'; 98 | $fieldset->add($field); 99 | 100 | // field email reply to 101 | $field = $this->modules->get('InputfieldText'); 102 | $field->name = 'emailReplyTo'; 103 | $field->label = __('Email: "Reply-To"-Address'); 104 | $field->notes = __('Optional; Scheme: "ReplyToName "'); 105 | $field->columnWidth = 34; 106 | $field->requiredIf = 'sendEmails=1'; 107 | $field->showIf = 'sendEmails=1'; 108 | $fieldset->add($field); 109 | $inputfields->add($fieldset); 110 | 111 | // fieldset general 112 | $fieldset = $this->modules->get('InputfieldFieldset'); 113 | $fieldset->label = __('Save Messages'); 114 | 115 | // save messages field 116 | $field = $this->modules->get('InputfieldCheckbox'); 117 | $field->name = 'saveMessages'; 118 | $field->label = __('Save Messages'); 119 | $field->description = __('Should the messages be saved?'); 120 | $field->value = 1; 121 | $field->columnWidth = 50; 122 | $fieldset->add($field); 123 | 124 | // save messages parent field 125 | $field = $this->modules->get('InputfieldPageListSelect'); 126 | $field->name = 'saveMessagesParent'; 127 | $field->label = __('Select a parent for items'); 128 | $field->description = __('All items created and managed will live under the parent you select here.'); 129 | $field->notes = __('If no parent is selected, items will be placed as children of the root page (not recommended).'); 130 | $field->required = 1; 131 | $field->requiredIf = 'saveMessages=1'; 132 | $field->showIf = 'saveMessages=1'; 133 | $field->columnWidth = 50; 134 | $fieldset->add($field); 135 | 136 | // save messages template field 137 | $field = $this->modules->get('InputfieldSelect'); 138 | $field->name = 'saveMessagesTemplate'; 139 | $field->label = __('Save messages template'); 140 | $field->description = __('Choose the template, that is used to save messages.'); 141 | foreach ($this->templates->getAll() as $key => $template) { 142 | if ($template->flags && Template::flagSystem) continue; 143 | $field->addOption($key, $template); 144 | } 145 | $field->required = 1; 146 | $field->requiredIf = 'saveMessages=1'; 147 | $field->showIf = 'saveMessages=1'; 148 | $field->columnWidth = 50; 149 | $fieldset->add($field); 150 | 151 | // save messages name scheme 152 | $field = $this->modules->get('InputfieldAsmSelect'); 153 | $field->description = __('Add all fields which should be used as part of the page name. Choose from existing ones, you may have to add them first below and save.'); 154 | $field->notes = __('The page name starts with a timestamp. All fields added above will be appended.'); 155 | $field->addOption('', ''); 156 | $field->label = __('Select page name fields'); 157 | $field->attr('name', 'saveMessagesScheme'); 158 | $field->showIf = 'saveMessages=1'; 159 | $field->columnWidth = 50; 160 | foreach ($allFields as $aField) { 161 | if ($f = $this->fields->get($aField)) { 162 | $field->addOption($f->name, $f->name); 163 | } 164 | } 165 | // $field->attr('value', $allFields); 166 | $fieldset->add($field); 167 | 168 | $inputfields->add($fieldset); 169 | 170 | // fieldset fields 171 | $fieldset = $this->modules->get('InputfieldFieldset'); 172 | $fieldset->label = __('Fields'); 173 | 174 | // allFields field 175 | $field = $this->modules->get('InputfieldAsmSelect'); 176 | $field->description = __('Add all fields (choose from existing ones) which should be attached to the form.'); 177 | $field->addOption('', ''); 178 | $field->label = __('Select form fields'); 179 | $field->attr('name', 'allFields'); 180 | $field->required = true; 181 | $field->columnWidth = 50; 182 | foreach ($this->fields as $f) { 183 | // skip system fields 184 | if ($f->flags & Field::flagSystem || $f->flags & Field::flagPermanent) continue; 185 | $field->addOption($f->name, $f->name); 186 | } 187 | $field->attr('value', $allFields); 188 | $fieldset->add($field); 189 | 190 | // field addFields 191 | $field = $this->modules->get('InputfieldText'); 192 | $field->name = 'addFields'; 193 | $field->label = __('Create and add fields'); 194 | $field->description = __('If you want to add non-existing fields, add them here as a comma-separated list.'); 195 | $field->columnWidth = 50; 196 | $fieldset->add($field); 197 | 198 | $inputfields->add($fieldset); 199 | 200 | // redirect to the same page to get rid of POST vars 201 | $field = $this->modules->get('InputfieldCheckbox'); 202 | $field->name = 'redirectSamePage'; 203 | $field->label = __('Redirect to the same page after successfull submission'); 204 | $field->description = __('OPTIONAL: If you prefer to stay on the same page without adding url parameter, just leave this field empty. Redirecting to the same page prevents form resubmission.'); 205 | $field->value = 1; 206 | $field->columnWidth = 50; 207 | $fieldset->add($field); 208 | 209 | // redirect to a specific URL after successfull submission 210 | $field = $this->modules->get('InputfieldPageListSelect'); 211 | $field->name = 'redirectPage'; 212 | $field->label = __('Redirect to a specific page after successfull submission '); 213 | $field->description = __('OPTIONAL: If you prefer to stay on the same page, just leave this field empty.'); 214 | $field->columnWidth = 50; 215 | $fieldset->add($field); 216 | 217 | // fieldset messages 218 | $fieldset = $this->modules->get('InputfieldFieldset'); 219 | $fieldset->label = __('Messages'); 220 | $fieldset->showIf = 'sendEmails=1'; 221 | $fieldset->collapsed = Inputfield::collapsedYes; 222 | 223 | // field email message 224 | $field = $this->modules->get('InputfieldTextarea'); 225 | $field->name = 'emailMessage'; 226 | $field->label = __('Email Message'); 227 | $field->description = __('Email message (optional - overwrites basic mail template).'); 228 | $field->notes = __('Use %fieldName% as placeholder, for example %fullName%.'); 229 | $field->rows = 5; 230 | $fieldset->add($field); 231 | 232 | $inputfields->add($fieldset); 233 | 234 | // fieldset spam 235 | $fieldset = $this->modules->get('InputfieldFieldset'); 236 | $fieldset->label = __('Spam'); 237 | $fieldset->collapsed = Inputfield::collapsedYes; 238 | 239 | // antiSpamTimeMin 240 | $field = $this->modules->get('InputfieldInteger'); 241 | $field->name = 'antiSpamTimeMin'; 242 | $field->label = __('Minimum Time'); 243 | $field->description = __('It parses the time the user needs to fill out the form. If the time is below a minimum time, the submission is treated as Spam.'); 244 | $field->notes = __('- in seconds -'); 245 | $field->columnWidth = 50; 246 | $fieldset->add($field); 247 | 248 | // antiSpamTimeMax 249 | $field = $this->modules->get('InputfieldInteger'); 250 | $field->name = 'antiSpamTimeMax'; 251 | $field->label = __('Maximum Time'); 252 | $field->description = __('It parses the time the user needs to fill out the form. If the time is over a maximum time, the submission is treated as Spam.'); 253 | $field->notes = __('- in seconds -'); 254 | $field->columnWidth = 50; 255 | $fieldset->add($field); 256 | 257 | // antiSpamPerDay 258 | $field = $this->modules->get('InputfieldInteger'); 259 | $field->name = 'antiSpamPerDay'; 260 | $field->label = __('Restrict Submissions'); 261 | $field->description = __('How often the form is allowed to be submitted by a single IP address in the last 24 hours.'); 262 | $field->columnWidth = 50; 263 | $fieldset->add($field); 264 | 265 | // antiSpamPerDay 266 | $field = $this->modules->get('InputfieldText'); 267 | $field->name = 'antiSpamExcludeIps'; 268 | $field->label = __('Exclude IPs'); 269 | $field->description = __('Comma-Seperated list of IP addresses to be excluded from IP filtering.'); 270 | $field->columnWidth = 50; 271 | $fieldset->add($field); 272 | 273 | $inputfields->add($fieldset); 274 | 275 | return $inputfields; 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /doc/overwrite-classes-and-markup.md: -------------------------------------------------------------------------------- 1 | # How to overwrite classes and markup 2 | 3 | Using the `markup` and `classes` option you'll be able to overwrite the default markup. 4 | 5 | **Example** 6 | 7 | ```php 8 | $scf = $modules->get('SimpleContactForm'); 9 | 10 | $options = array( 11 | 'markup' => array( 12 | 'list' => "
{out}
", 13 | 'item' => "

{out}

" 14 | ), 15 | 'classes' => array( 16 | 'form' => 'form form__whatever', 17 | 'list' => 'list-item' 18 | ) 19 | ); 20 | 21 | echo $scf->render($options); 22 | ``` 23 | 24 | If you need additional markup before or after the form, you can use `prependMarkup` and `appendMarkup`. 25 | 26 | Below is the list of all available customization options copied from [ProcessWire master][1]. 27 | 28 | ```php 29 | /** 30 | * Markup used during the render() method 31 | * 32 | */ 33 | static protected $defaultMarkup = array( 34 | 'list' => "{out}\n", 35 | 'item' => "\n\t
\n{out}\n\t
", 36 | 'item_label' => "\n\t\t", 37 | 'item_label_hidden' => "\n\t\t", 38 | 'item_content' => "{out}", 39 | 'item_error' => "\n

{out}

", 40 | 'item_description' => "\n

{out}

", 41 | 'item_head' => "\n

{out}

", 42 | 'item_notes' => "\n

{out}

", 43 | 'item_icon' => "", 44 | 'item_toggle' => "", 45 | // ALSO: 46 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 47 | ); 48 | 49 | /** 50 | * Classes used during the render() method 51 | * 52 | */ 53 | static protected $defaultClasses = array( 54 | 'form' => 'form js-simplecontactform', // additional clases for inputfieldform (optional) 55 | 'form_error' => 'form--error--message', 56 | 'form_success' => 'form--success--message', 57 | 'list' => 'fields', 58 | 'list_clearfix' => 'clearfix', 59 | 'item' => 'form__item form__item--{name}', 60 | 'item_label' => '', // additional classes for inputfieldheader (optional) 61 | 'item_content' => '', // additional classes for inputfieldcontent (optional) 62 | 'item_required' => 'field--required', // class is for inputfield 63 | 'item_error' => 'field--error', // note: not the same as markup[item_error], class is for inputfield 64 | 'item_collapsed' => 'field--collapsed', 65 | 'item_column_width' => 'field__column', 66 | 'item_column_width_first' => 'field__column--first', 67 | 'item_show_if' => 'field--show-if', 68 | 'item_required_if' => 'field--required-if' 69 | // ALSO: 70 | // InputfieldAnything => array( any of the properties above to override on a per-Inputifeld basis) 71 | ); 72 | ``` 73 | 74 | ## Trouble Shooting 75 | 76 | Normally you're able to override the markup on a per-Intputfield basis like mentioned above: 77 | 78 | ```php 79 | 'markup' => array( 80 | // @see: https://github.com/processwire/ProcessWire/blob/master/wire/core/InputfieldWrapper.php#L44 81 | 'InputfieldSubmit' => array( 82 | // any of the properties above to override on a per-Inputifeld basis 83 | ) 84 | ), 85 | ``` 86 | 87 | Example: 88 | 89 | ```php 90 | $scf = $modules->get('SimpleContactForm'); 91 | 92 | $options = array( 93 | 'btnClass' => 'btn btn-blue btn-effect', 94 | 'btnText' => 'Send', 95 | 'classes' => array( 96 | 'item' => 'input-field' 97 | ) 98 | ); 99 | 100 | $content .= $scf->render($options); 101 | ``` 102 | 103 | However this doesn't seem to work in some cases (using InputfielSubmit or InputfieldButton). 104 | But you can override the `render` function of the specific class, in this example `InputfieldSubmit` (for example in `init.php`): 105 | 106 | ```php 107 | $this->addHook('InputfieldSubmit::render', function(HookEvent $event) { 108 | if ($this->page->template->name === 'contact') { // adapt template name to compare with 109 | $parent = (object)$event->object; 110 | $attrs = $parent->getAttributesString(); 111 | $value = $parent->entityEncode($parent->attr('value')); 112 | $out = ""; 113 | $event->return = $out; 114 | } 115 | }); 116 | ``` 117 | 118 | One more example: 119 | 120 | ```php 121 | $this->addHookBefore('Inputfield::render', function(HookEvent $event) { 122 | if ($this->page->template->name === 'contact') { // adapt template name to compare with 123 | $inputfield = $event->object; 124 | $inputfield->addClass('col-sm-8'); 125 | $event->return = $inputfield; 126 | } 127 | }); 128 | ``` 129 | 130 | [1]: https://github.com/processwire/ProcessWire/blob/master/wire/core/InputfieldWrapper.php#L44 'ProcessWire master' 131 | -------------------------------------------------------------------------------- /doc/spam-translate.md: -------------------------------------------------------------------------------- 1 | # How to translate the spam message 2 | 3 | You could translate the values  – which will be inserted – with 'spam' and 'time'. 4 | 5 | Or, you could also translate *"Sorry, but your message didn't pass our %s test. Please try another %s."* 6 | with *"Sorry, but your message didn't pass our spam test. Please try another time."*. 7 | -------------------------------------------------------------------------------- /doc/success-message.md: -------------------------------------------------------------------------------- 1 | # How to add a custom success message 2 | 3 | ## Option 1 4 | 5 | Simple set it as option parameter: 6 | 7 | ``` php 8 | $modules->get('SimpleContactForm')->render(array( 9 | 'emailMessage' => 'Custom Message' 10 | )); 11 | ``` 12 | 13 | ## Option 2 14 | 15 | Make it translatable: 16 | 17 | ``` php 18 | $modules->get('SimpleContactForm')->render(array( 19 | 'emailMessage' => __('Custom Message') 20 | )); 21 | ``` 22 | 23 | ## Option 3 24 | 25 | Set it as variable before: 26 | 27 | ``` php 28 | $emailMessage = $page->title . ' test'; 29 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 30 | ``` 31 | 32 | ## Option 4 33 | 34 | Include the content from another file: 35 | 36 | ``` php 37 | include('./message.php'); 38 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 39 | 40 | // message.php 41 | title . ' some content ' . $input->post->fieldname; 44 | ``` 45 | 46 | ## Option 5 47 | 48 | Include the content from another file using output buffering: 49 | 50 | ``` php 51 | ob_start(); 52 | include('./message.php'); 53 | $emailMessage = ob_get_clean(); 54 | $content .= $modules->get('SimpleContactForm')->render(array('emailMessage' => $emailMessage)); 55 | 56 | // message.php 57 | title . ' test3 ' . $input->post->headline; 59 | ``` 60 | 61 | ## Using twig 62 | 63 | ```html 64 | {% set emailMessage %}{% include 'mails/recommend.twig' %}{% endset %} 65 | {% set options = { 66 | 'action': './#recommend', 67 | 'emailMessage': emailMessage 68 | } %} 69 | {{modules.get('SimpleContactForm').render(options)}} 70 | 71 | {# recommend.twig #} 72 | ... 73 | {{ estate.title }} 74 | {{ page.httpUrl }} 75 | {% if input.scf_salutation == 1 %}xx{% elseif input.scf_salutation == 2 %}xx{% else %}xx{% endif %} 76 | ... 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/Mailer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2017 8 | * @filesource 9 | */ 10 | 11 | /** 12 | * Class Mailer 13 | */ 14 | class Mailer extends \ProcessWire\Wire { 15 | 16 | /** 17 | * construct 18 | * 19 | * @param string $to 20 | * @param string $from 21 | * @param string $replyTo 22 | * @param string $subject 23 | * @param string $body 24 | */ 25 | public function __construct($to, $from, $replyTo, $subject, $body) { 26 | $this->subject = $subject; 27 | $this->body = $body; 28 | 29 | $this->to = array(); 30 | foreach (explode(',', $to) as $value) { 31 | list($toEmail, $toName) = $this->extractEmailAndName($value); 32 | $this->to[] = "$toName <$toEmail>"; 33 | } 34 | 35 | list($fromEmail, $fromName) = $this->extractEmailAndName($from); 36 | $this->from = "$fromName <$fromEmail>"; 37 | 38 | if ($replyTo) { 39 | list($replyToEmail, $replyToName) = $this->extractEmailAndName($replyTo); 40 | $this->replyTo = "$replyToName <$replyToEmail>"; 41 | } else { 42 | $this->replyTo = ''; 43 | } 44 | } 45 | 46 | /** 47 | * extract email from name 48 | * substitute umlaute 49 | * 50 | * @param string $email 51 | * @return array 52 | */ 53 | protected function extractEmailAndName($email) { 54 | $name = ''; 55 | if (strpos($email, '<') !== false && strpos($email, '>') !== false) { 56 | // email has separate from name and email 57 | if (preg_match('/^(.*?)<([^>]+)>.*$/', $email, $matches)) { 58 | $name = preg_replace( 59 | array('/ä/', '/ö/', '/ü/', '/Ä/', '/Ö/', '/Ü/','/ß/'), 60 | array('ae', 'oe', 'ue', 'Ae', 'Oe', 'Ue', 'ss'), 61 | $matches[1] 62 | ); 63 | $email = $matches[2]; 64 | } 65 | } 66 | 67 | return array($email, $name); 68 | } 69 | 70 | /** 71 | * send mail 72 | * 73 | * @return boolean 74 | */ 75 | public function send() { 76 | $wireMail = \ProcessWire\wireMail(); // don't use `new WireMail()` which bypasses WireMailSMTP 77 | 78 | $wireMail->to($this->to); 79 | $wireMail->from($this->from); 80 | $wireMail->subject($this->subject); 81 | $wireMail->body($this->body); 82 | 83 | if ($this->replyTo) $wireMail->header('Reply-To', $this->replyTo); 84 | 85 | return $wireMail->send(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /lib/SpamProtection.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2017 11 | * @filesource 12 | */ 13 | 14 | /** 15 | * Class SpamProtection 16 | */ 17 | class SpamProtection extends \ProcessWire\Wire { 18 | 19 | /** 20 | * User agents marked as spam 21 | */ 22 | const USER_AGENTS = '#w3c|google|slurp|msn|yahoo|y!j|altavista|ask|spider|search|bot|crawl|usw#i'; 23 | 24 | /** 25 | * boolean isSpam 26 | */ 27 | protected static $isSpam = false; 28 | 29 | /** 30 | * Error messages used in log file 31 | */ 32 | protected static $errorMessages = array( 33 | 'default' => 'An error occured.', 34 | 'token' => 'CSRF Token validation failed.', 35 | 'honeypot' => 'Honeypot field was filled.', 36 | 'numberOfFields' => 'Number of fields does not match.', 37 | 'userAgent' => 'User Agent is not allowed.', 38 | 'httpParams' => 'User Agent and HTTP Referer are empty.', 39 | 'timeRange' => 'Date difference is out of range.', 40 | 'ipAddress' => 'This IP address was already marked as spam.', 41 | 'numberOfSubmits' => 'This IP address submitted this form too often.' 42 | ); 43 | 44 | /** 45 | * Is valid checks 46 | * Not depending on whether messages should be saved 47 | */ 48 | protected static $isValidChecks = array( 49 | 'validToken', 50 | 'validHoneypot', 51 | 'validNumberOfFields', 52 | 'validUserAgent', 53 | 'validHttpParams', 54 | 'validTimeRange' 55 | ); 56 | 57 | /** 58 | * Is valid save messages checks 59 | * Depending on whether messages should be saved 60 | */ 61 | protected static $isValidSMChecks = array( 62 | 'validIpAddress', 63 | 'validNumberOfSubmits' 64 | ); 65 | 66 | /** 67 | * Construct 68 | */ 69 | public function __construct() { 70 | $this->setLogFile(); 71 | $this->currentIp = $_SERVER['REMOTE_ADDR']; 72 | } 73 | 74 | /** 75 | * Set log file 76 | */ 77 | public function setLogFile() { 78 | $this->scfLog = strtolower(SCF::CLASS_NAME . '-log'); 79 | } 80 | 81 | /** 82 | * Set number of inputs to compare with 83 | * 84 | * @param integer $count 85 | * @return SpamProtection 86 | */ 87 | public function setCount($count) { 88 | $this->count = (int)$count; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set time range a submission is valid 94 | * 95 | * @param integer $min 96 | * @param integer $max 97 | * @return SpamProtection 98 | */ 99 | public function setTimeRange($min, $max) { 100 | $this->timeRange = (object)array( 101 | 'min' => $min, 102 | 'max' => $max 103 | ); 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set whether messages should be saved 109 | * 110 | * @param boolean $saveMessages 111 | * @return SpamProtection 112 | */ 113 | public function setSaveMessages($saveMessages) { 114 | $this->saveMessages = $saveMessages; 115 | return $this; 116 | } 117 | 118 | /** 119 | * Set ip addresses which should be excluded 120 | * 121 | * @param string $excludeIpAdresses 122 | * @return SpamProtection 123 | */ 124 | public function setExcludeIpAdresses($excludeIpAdresses) { 125 | $this->excludeIpAdresses = explode(',', $excludeIpAdresses); 126 | return $this; 127 | } 128 | 129 | /** 130 | * Set maximum number of submits per day 131 | * 132 | * @param string $numberOfSubmitsPerDay 133 | * @return SpamProtection 134 | */ 135 | public function setNumberOfSubmitsPerDay($numberOfSubmitsPerDay) { 136 | $this->numberOfSubmitsPerDay = $numberOfSubmitsPerDay; 137 | return $this; 138 | } 139 | 140 | /** 141 | * Whether spam was detected 142 | * 143 | * @return boolean 144 | */ 145 | public function isSpam() { 146 | return self::$isSpam; 147 | } 148 | 149 | /** 150 | * Get random animal to build error message 151 | * 152 | * @return string 153 | */ 154 | public function getAnimal() { 155 | $animals = array( 156 | $this->_('monkey'), 157 | $this->_('squirrel'), 158 | $this->_('giraffe'), 159 | $this->_('marmot') 160 | ); 161 | 162 | return $animals[array_rand($animals)]; 163 | } 164 | 165 | /** 166 | * Get random fruit to build error message 167 | * 168 | * @return string 169 | */ 170 | public function getFruit() { 171 | $fruits = array( 172 | $this->_('strawberry'), 173 | $this->_('banana'), 174 | $this->_('peanut'), 175 | $this->_('blueberry') 176 | ); 177 | 178 | return $fruits[array_rand($fruits)]; 179 | } 180 | 181 | /** 182 | * Set whether the request is marked as spam 183 | * 184 | * @param boolean $isSpam 185 | */ 186 | protected function setIsSpam($isSpam = true) { 187 | self::$isSpam = $isSpam; 188 | } 189 | 190 | /** 191 | * Add log entry 192 | * 193 | * @param string $key 194 | */ 195 | protected function addLogEntry($key) { 196 | $this->log->save($this->scfLog, "[FAILURE] {$this->getErrorMessage($key)} IP: {$this->currentIp}"); 197 | } 198 | 199 | /** 200 | * Get specific error message 201 | * 202 | * @param string $key 203 | * @return string 204 | */ 205 | protected function getErrorMessage($key) { 206 | if (!array_key_exists($key, self::$errorMessages)) $key = 'default'; 207 | return self::$errorMessages[$key]; 208 | } 209 | 210 | /** 211 | * Check CSRF token 212 | */ 213 | protected function validToken() { 214 | try { 215 | $this->session->CSRF->validate(); 216 | } catch (WireCSRFException $e) { 217 | $this->setIsSpam(); 218 | $this->addLogEntry('token'); 219 | } 220 | } 221 | 222 | /** 223 | * Check if the honeypot field was filled 224 | */ 225 | protected function validHoneypot() { 226 | if ($this->input->post->{'scf-website'}) { 227 | $this->setIsSpam(); 228 | $this->addLogEntry('honeypot'); 229 | } 230 | } 231 | 232 | /** 233 | * Check if the number of fields match 234 | */ 235 | protected function validNumberOfFields() { 236 | if (count($this->input->post) !== $this->count) { 237 | $this->setIsSpam(); 238 | $this->addLogEntry('numberOfFields'); 239 | } 240 | } 241 | 242 | /** 243 | * Check the user agent 244 | */ 245 | protected function validUserAgent() { 246 | if (preg_match(self::USER_AGENTS, $_SERVER['HTTP_USER_AGENT'])) { 247 | $this->setIsSpam(); 248 | $this->addLogEntry('userAgent'); 249 | } 250 | } 251 | 252 | /** 253 | * Check http referrer and user agent 254 | */ 255 | protected function validHttpParams() { 256 | if ($_SERVER['HTTP_REFERER'] === '' && $_SERVER['HTTP_USER_AGENT'] === '') { 257 | $this->setIsSpam(); 258 | $this->addLogEntry('httpParams'); 259 | } 260 | } 261 | 262 | /** 263 | * Check whether the form was submitted within a certain time range 264 | */ 265 | protected function validTimeRange() { 266 | $date = (int)$this->input->post->{'scf-date'}; 267 | $dateDiff = $date ? time() - $date : 0; 268 | if ($dateDiff <= $this->timeRange->min || $dateDiff >= $this->timeRange->max) { 269 | $this->setIsSpam(); 270 | $this->addLogEntry('timeRange'); 271 | } 272 | } 273 | 274 | /** 275 | * Check whether the ip address was marked as spam 276 | */ 277 | protected function validIpAddress() { 278 | $spamIpPages = $this->pages->find('template=' . SCF::SM_TEMPLATE_NAME . ', scf_spamIp!='); 279 | if (!$spamIpPages->count()) return; 280 | 281 | foreach ($spamIpPages as $spamIpPage) { 282 | if ($spamIpPage->scf_ip === $this->currentIp) { 283 | $this->setIsSpam(); 284 | $this->addLogEntry('ipAddress'); 285 | break; 286 | } 287 | } 288 | } 289 | 290 | /** 291 | * Check how often the form is allowed to be submitted by a single IP address 292 | */ 293 | protected function validNumberOfSubmits() { 294 | $dateSub = new \DateTime(); 295 | $dateSub->sub(new \DateInterval('P1D')); 296 | $selector = 'template=' . SCF::SM_TEMPLATE_NAME . ", scf_ip={$this->currentIp}, scf_date>={$dateSub->getTimestamp()}"; 297 | $totalLast24h = $this->pages->find($selector)->count(); 298 | 299 | if ($totalLast24h >= $this->numberOfSubmitsPerDay) { 300 | $this->setIsSpam(); 301 | $this->addLogEntry('numberOfSubmits'); 302 | } 303 | } 304 | 305 | /** 306 | * Validates the form 307 | * 308 | * @return SimpleContactForm 309 | */ 310 | public function validate() { 311 | foreach (self::$isValidChecks as $isValid) { 312 | $this->$isValid(); 313 | if (self::$isSpam) break; 314 | } 315 | 316 | // additional checks only if save messages feature is turned on 317 | if (!$this->isSpam() && $this->saveMessages) { 318 | if (!in_array($this->currentIp, $this->excludeIpAdresses)) { 319 | foreach (self::$isValidSMChecks as $isValid) { 320 | $this->$isValid(); 321 | if (self::$isSpam) break; 322 | } 323 | } 324 | } 325 | 326 | return $this; 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /resources/contact.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | const contact = () => { 4 | 5 | // set a global jquery function for submitting the form 6 | $.simplecontactform = () => { 7 | const $forms = $('.js-simplecontactform'); 8 | 9 | if ($forms.length) { 10 | // reinit the form-ajax submission 11 | $forms.off('submit.simplecontactform'); 12 | $forms.on('submit.simplecontactform', (e) => { 13 | const $form = $(e.currentTarget); 14 | 15 | e.preventDefault(); 16 | $.post(e.target.action, `${$form.serialize()}&submit=submit`, (data) => { 17 | $form.replaceWith($(data)); 18 | $.simplecontactform(); 19 | }); 20 | }); 21 | } 22 | }; 23 | 24 | // and run it once 25 | $.simplecontactform(); 26 | 27 | }; 28 | 29 | export default contact; 30 | -------------------------------------------------------------------------------- /resources/jquery.simplecontactform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Plugin 3 | * Author: Tabea David | tabea.david@kf-interactive.com 4 | */ 5 | 6 | ;(function($, window, document, undefined) { 7 | 8 | $.simplecontactform = function($element, options) { 9 | 10 | // some default vars and self reference for scope issues 11 | var plugin = {}; 12 | plugin.options = $.extend({}, $.simplecontactform.defaults, options); 13 | plugin.$forms = $element; 14 | 15 | // all plugin methods 16 | plugin = $.extend(plugin, { 17 | 18 | load: function() { 19 | if (plugin.$forms.length) { 20 | // reinit the form-ajax submission 21 | plugin.$forms.off('submit.simplecontactform'); 22 | plugin.$forms.on('submit.simplecontactform', function (e) { 23 | var $form = $(this); 24 | e.preventDefault(); 25 | 26 | $.post(e.target.action, $form.serialize() + '&submit=submit', function (data) { 27 | $form.parent().replaceWith($(data)); 28 | plugin.load(); 29 | }); 30 | }); 31 | } 32 | } 33 | 34 | 35 | }); 36 | 37 | // run the plugin 38 | // ====================================================================== 39 | plugin.load(); 40 | // ====================================================================== 41 | }; 42 | 43 | 44 | // define the plugin defaults here 45 | $.simplecontactform.defaults = { 46 | form: 'js-simplecontactform' 47 | }; 48 | 49 | 50 | // jquery wrapper function 51 | $.fn.simplecontactform = function(options) { 52 | return this.each(function() { 53 | var simplecontactform = $.simplecontactform(this, options); 54 | }); 55 | }; 56 | 57 | }(jQuery, window, document)); 58 | --------------------------------------------------------------------------------