blib/lib/CGI/ContactForm.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 26 | 287 | 9.0 |
branch | 1 | 146 | 0.6 |
condition | 0 | 92 | 0.0 |
subroutine | 9 | 34 | 26.4 |
pod | 0 | 18 | 0.0 |
total | 36 | 577 | 6.2 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package CGI::ContactForm; | ||||||
2 | |||||||
3 | $VERSION = '1.52'; | ||||||
4 | # $Id: ContactForm.pm,v 1.76 2009/03/03 22:46:53 gunnarh Exp $ | ||||||
5 | |||||||
6 | =head1 NAME | ||||||
7 | |||||||
8 | CGI::ContactForm - Generate a web contact form | ||||||
9 | |||||||
10 | =head1 SYNOPSIS | ||||||
11 | |||||||
12 | use CGI::ContactForm; | ||||||
13 | |||||||
14 | contactform ( | ||||||
15 | recname => 'John Smith', | ||||||
16 | recmail => 'john.smith@example.com', | ||||||
17 | styleurl => '/style/ContactForm.css', | ||||||
18 | ); | ||||||
19 | |||||||
20 | =head1 DESCRIPTION | ||||||
21 | |||||||
22 | This module generates a contact form for the web when the routine C |
||||||
23 | is called from a CGI script. Arguments are passed to the module as a list of | ||||||
24 | key/value pairs. | ||||||
25 | |||||||
26 | C |
||||||
27 | with RFC 2646) email message, with the sender's address in the C |
||||||
28 | |||||||
29 | By default the sender gets a C |
||||||
30 | sender is invalid, by default the failure message is sent to the recipient address, | ||||||
31 | through which you know that you don't need to bother with a reply, at least not to | ||||||
32 | that address... However, by setting the C |
||||||
33 | sender copy from being sent. | ||||||
34 | |||||||
35 | =head2 Arguments | ||||||
36 | |||||||
37 | C |
||||||
38 | |||||||
39 | Default value | ||||||
40 | ============= | ||||||
41 | Compulsory | ||||||
42 | ---------- | ||||||
43 | recname (none) | ||||||
44 | recmail (none) | ||||||
45 | |||||||
46 | Optional | ||||||
47 | -------- | ||||||
48 | smtp 'localhost' | ||||||
49 | styleurl (none) | ||||||
50 | returnlinktext 'Main Page' | ||||||
51 | returnlinkurl '/' | ||||||
52 | subject (none) | ||||||
53 | nocopy 0 | ||||||
54 | bouncetosender 0 | ||||||
55 | formtmplpath (none) | ||||||
56 | resulttmplpath (none) | ||||||
57 | maxsize 100 (KiB) | ||||||
58 | maxperhour 5 (messages per hour per host) | ||||||
59 | tempdir (none) | ||||||
60 | spamfilter '(?is:|\[/url]|https?:/(?:.+https?:/){3})' (Perl regex) | ||||||
61 | |||||||
62 | Additional arguments, intended for forms at non-English sites | ||||||
63 | ------------------------------------------------------------- | ||||||
64 | title 'Send email to' | ||||||
65 | namelabel 'Your name:' | ||||||
66 | emaillabel 'Your email:' | ||||||
67 | subjectlabel 'Subject:' | ||||||
68 | msglabel 'Message:' | ||||||
69 | reset 'Reset' | ||||||
70 | send 'Send' | ||||||
71 | erroralert 'Fields with %s need to be filled or corrected.' | ||||||
72 | marked 'marked labels' | ||||||
73 | thanks 'Thanks for your message!' | ||||||
74 | sent_to 'The message was sent to %s with a copy to %s.' | ||||||
75 | sent_to_short 'The message was sent to %s.' | ||||||
76 | encoding 'ISO-8859-1' | ||||||
77 | |||||||
78 | =head2 Customization | ||||||
79 | |||||||
80 | There are only two compulsory arguments. The example CGI script | ||||||
81 | C |
||||||
82 | argument, assuming the use of the enclosed style sheet C |
||||||
83 | That results in a decently styled form with a minimum of effort. | ||||||
84 | |||||||
85 | If the default value C |
||||||
86 | server, you may need to explicitly state its host name or IP address via the | ||||||
87 | C |
||||||
88 | |||||||
89 | As you can see from the list over available arguments, all the text strings | ||||||
90 | can be changed, and as regards the presentation, you can of course edit the | ||||||
91 | style sheet to your liking. | ||||||
92 | |||||||
93 | If you want to modify the HTML markup, you can have C |
||||||
94 | use of one or two templates. The enclosed example templates | ||||||
95 | C |
||||||
96 | the C |
||||||
97 | starting point for a customized markup. | ||||||
98 | |||||||
99 | =head2 Spam prevention | ||||||
100 | |||||||
101 | Behind the scenes C |
||||||
102 | and/or discourage abuse in the form of submitted spam messages. | ||||||
103 | |||||||
104 | =over 4 | ||||||
105 | |||||||
106 | =item * | ||||||
107 | |||||||
108 | The number of messages that can be sent from the same host is restricted. The | ||||||
109 | default is 5 messages per hour. | ||||||
110 | |||||||
111 | =item * | ||||||
112 | |||||||
113 | A customizable spamfilter is applied to the body of the message. By default it | ||||||
114 | allows max 3 URLs that start with C |
||||||
115 | submissions with C |
||||||
116 | |||||||
117 | =item * | ||||||
118 | |||||||
119 | When sending a message, the request must include a cookie. | ||||||
120 | |||||||
121 | =back | ||||||
122 | |||||||
123 | The thought is that normal use, i.e. establishing contact with somebody, | ||||||
124 | should typically not be affected by those checks. | ||||||
125 | |||||||
126 | =head1 INSTALLATION | ||||||
127 | |||||||
128 | =head2 Installation with Makefile.PL | ||||||
129 | |||||||
130 | Type the following: | ||||||
131 | |||||||
132 | perl Makefile.PL | ||||||
133 | make | ||||||
134 | make install | ||||||
135 | |||||||
136 | =head2 Manual Installation | ||||||
137 | |||||||
138 | =over 4 | ||||||
139 | |||||||
140 | =item * | ||||||
141 | |||||||
142 | Download the distribution file and extract the contents. | ||||||
143 | |||||||
144 | =item * | ||||||
145 | |||||||
146 | Designate a directory as your local library for Perl modules, for instance | ||||||
147 | |||||||
148 | /www/username/cgi-bin/lib | ||||||
149 | |||||||
150 | =item * | ||||||
151 | |||||||
152 | Create the directory C, and upload | ||||||
153 | C |
||||||
154 | |||||||
155 | =item * | ||||||
156 | |||||||
157 | Create the directory C, and | ||||||
158 | upload C |
||||||
159 | |||||||
160 | =item * | ||||||
161 | |||||||
162 | In the CGI scripts that use this module, include a line that tells Perl | ||||||
163 | to look for modules also in your local library, such as | ||||||
164 | |||||||
165 | use lib '/www/username/cgi-bin/lib'; | ||||||
166 | |||||||
167 | =back | ||||||
168 | |||||||
169 | =head2 Other Installation Matters | ||||||
170 | |||||||
171 | If you have previous experience from installing CGI scripts, making | ||||||
172 | C |
||||||
173 | Otherwise, this is a B |
||||||
174 | |||||||
175 | =over 4 | ||||||
176 | |||||||
177 | =item 1. | ||||||
178 | |||||||
179 | Upload the CGI file in ASCII transfer mode to your C |
||||||
180 | |||||||
181 | =item 2. | ||||||
182 | |||||||
183 | Set the file permission 755 (chmod 755). | ||||||
184 | |||||||
185 | =back | ||||||
186 | |||||||
187 | If that doesn't do it, there are many CGI tutorials for beginners | ||||||
188 | available on the web. This is one example: | ||||||
189 | |||||||
190 | http://my.execpc.com/~keithp/bdlogcgi.htm | ||||||
191 | |||||||
192 | On some servers, the CGI file must be located in the C |
||||||
193 | (or in a C |
||||||
194 | that the style sheet typically needs to be located somewhere outside the | ||||||
195 | C |
||||||
196 | |||||||
197 | =head1 DEPENDENCIES | ||||||
198 | |||||||
199 | C |
||||||
200 | L |
||||||
201 | |||||||
202 | =head1 AUTHENTICATION | ||||||
203 | |||||||
204 | If you have access to a mail server that is configured to automatically | ||||||
205 | accept sending messages from a CGI script to any address, you don't need | ||||||
206 | to worry about authentication. Otherwise you need to somehow authenticate | ||||||
207 | to the server, for instance by adding something like this right after the | ||||||
208 | C | ||||||
209 | |||||||
210 | %Mail::Sender::default = ( | ||||||
211 | auth => 'LOGIN', | ||||||
212 | authid => 'username', | ||||||
213 | authpwd => 'password', | ||||||
214 | ); | ||||||
215 | |||||||
216 | C |
||||||
217 | and C |
||||||
218 | find out which protocol and username/password pair to use. | ||||||
219 | |||||||
220 | If there are multiple forms, a more convenient way to deal with a need | ||||||
221 | for authentication may be to make use of the C |
||||||
222 | is included in the distribution. You just edit it and upload it to the | ||||||
223 | same directory as the one where C |
||||||
224 | |||||||
225 | See the L |
||||||
226 | |||||||
227 | =head1 AUTHOR, COPYRIGHT AND LICENSE | ||||||
228 | |||||||
229 | Copyright (c) 2003-2019 Gunnar Hjalmarsson | ||||||
230 | http://www.gunnar.cc/cgi-bin/contact.pl | ||||||
231 | |||||||
232 | This module is free software; you can redistribute it and/or modify it | ||||||
233 | under the same terms as Perl itself. | ||||||
234 | |||||||
235 | =head1 SEE ALSO | ||||||
236 | |||||||
237 | L |
||||||
238 | L |
||||||
239 | |||||||
240 | =cut | ||||||
241 | |||||||
242 | 1 | 1 | 5554 | use strict; | |||
1 | 2 | ||||||
1 | 32 | ||||||
243 | 1 | 1 | 854 | use CGI 'escapeHTML'; | |||
1 | 33473 | ||||||
1 | 7 | ||||||
244 | 1 | 1 | 134 | use File::Basename; | |||
1 | 2 | ||||||
1 | 114 | ||||||
245 | 1 | 1 | 7 | use File::Spec; | |||
1 | 4 | ||||||
1 | 24 | ||||||
246 | 1 | 1 | 5 | use Fcntl qw(:DEFAULT :flock); | |||
1 | 2 | ||||||
1 | 390 | ||||||
247 | 1 | 1 | 7 | use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); | |||
1 | 2 | ||||||
1 | 73 | ||||||
248 | 1 | 1 | 6 | use Exporter; | |||
1 | 2 | ||||||
1 | 163 | ||||||
249 | @ISA = 'Exporter'; | ||||||
250 | @EXPORT = 'contactform'; | ||||||
251 | @EXPORT_OK = 'CFdie'; | ||||||
252 | |||||||
253 | BEGIN { | ||||||
254 | sub CFdie($) { | ||||||
255 | 0 | 0 | 0 | print "Status: 400 Bad Request\n"; | |||
256 | 0 | print "Content-type: text/html\n\nError\n", shift; |
|||||
257 | 0 | 0 | if ( $ENV{MOD_PERL} ) { | ||||
258 | 0 | 0 | if ( $] < 5.006 ) { | ||||
259 | 0 | require Apache; | |||||
260 | 0 | Apache::exit(); | |||||
261 | } | ||||||
262 | } | ||||||
263 | 0 | exit 1; | |||||
264 | } | ||||||
265 | |||||||
266 | 1 | 1 | 81 | eval "use Mail::Sender"; | |||
1 | 1 | 882 | |||||
1 | 119583 | ||||||
1 | 49 | ||||||
267 | 1 | 50 | 4359 | CFdie($@) if $@; | |||
268 | } | ||||||
269 | |||||||
270 | sub contactform { | ||||||
271 | 0 | 0 | 0 | local $^W = 1; # enables warnings | |||
272 | 0 | my ($error, $in) = {}; | |||||
273 | 0 | my $time = time; | |||||
274 | 0 | 0 | my $host = $ENV{'REMOTE_ADDR'} or die "REMOTE_ADDR not set\n"; | ||||
275 | 0 | umask 0; | |||||
276 | 0 | my $args = &arguments; | |||||
277 | 0 | 0 | if ($ENV{REQUEST_METHOD} eq 'POST') { | ||||
278 | 0 | checktimestamp( $args->{tempdir}, $time ); | |||||
279 | 0 | $in = formdata( $args->{maxsize} ); | |||||
280 | 0 | 0 | if (formcheck($in, $args->{subject}, $error) == 0) { | ||||
281 | 0 | checkspamfilter( $in->{message}, $args->{spamfilter} ); | |||||
282 | 0 | checkmaxperhour($args, $time, $host); | |||||
283 | 0 | eval { mailsend($args, $in, $host) }; | |||||
0 | |||||||
284 | 0 | 0 | CFdie( escapeHTML(my $msg = $@) ) if $@; | ||||
285 | 0 | return; | |||||
286 | } | ||||||
287 | } else { | ||||||
288 | 0 | settimestamp( $args->{tempdir}, $time ); | |||||
289 | } | ||||||
290 | 0 | formprint($args, $in, $error); | |||||
291 | } | ||||||
292 | |||||||
293 | sub arguments { | ||||||
294 | 0 | 0 | 0 | my %defaults = ( | |||
295 | recname => '', | ||||||
296 | recmail => '', | ||||||
297 | smtp => 'localhost', | ||||||
298 | styleurl => '', | ||||||
299 | returnlinktext => 'Main Page', | ||||||
300 | returnlinkurl => '/', | ||||||
301 | subject => '', | ||||||
302 | nocopy => 0, | ||||||
303 | bouncetosender => 0, | ||||||
304 | formtmplpath => '', | ||||||
305 | resulttmplpath => '', | ||||||
306 | maxsize => 100, | ||||||
307 | maxperhour => 5, | ||||||
308 | tempdir => '', | ||||||
309 | spamfilter => '(?is:|\[/url]|https?:/(?:.+https?:/){3})', | ||||||
310 | title => 'Send email to', | ||||||
311 | namelabel => 'Your name:', | ||||||
312 | emaillabel => 'Your email:', | ||||||
313 | subjectlabel => 'Subject:', | ||||||
314 | msglabel => 'Message:', | ||||||
315 | reset => 'Reset', | ||||||
316 | send => 'Send', | ||||||
317 | erroralert => 'Fields with %s need to be filled or corrected.', | ||||||
318 | marked => 'marked labels', | ||||||
319 | thanks => 'Thanks for your message!', | ||||||
320 | sent_to => 'The message was sent to %s with a copy to %s.', | ||||||
321 | sent_to_short => 'The message was sent to %s.', | ||||||
322 | encoding => 'ISO-8859-1', | ||||||
323 | ); | ||||||
324 | 0 | my $error; | |||||
325 | 0 | 0 | if ( @_ % 2 ) { | ||||
326 | 0 | $error .= "Odd number of elements in argument list:\n" | |||||
327 | . " The contactform() function expects a number of key/value pairs.\n"; | ||||||
328 | } | ||||||
329 | 0 | my %args = ( %defaults, @_ ); | |||||
330 | 0 | for (qw/recname recmail/) { | |||||
331 | 0 | 0 | $error .= "The compulsory argument '$_' is missing.\n" unless $args{$_}; | ||||
332 | } | ||||||
333 | 0 | for (keys %args) { | |||||
334 | 0 | 0 | $error .= "Unknown argument: '$_'\n" unless defined $defaults{$_}; | ||||
335 | } | ||||||
336 | 0 | 0 | 0 | if ($args{recmail} and emailsyntax($args{recmail})) { | |||
337 | 0 | $error .= "'$args{recmail}' is not a valid email address.\n"; | |||||
338 | } | ||||||
339 | 0 | 0 | 0 | unless ($args{tempdir}) { | |||
340 | 0 | 0 | 0 | unless (-d $CGITempFile::TMPDIRECTORY and -w _ and -x _) { | |||
0 | |||||||
341 | 0 | $error .= "You need to state a temporary directory via the 'tempdir' argument.\n"; | |||||
342 | } | ||||||
343 | } elsif (!(-d $args{tempdir} and -w _ and -x _)) { | ||||||
344 | $error .= "'$args{tempdir}' is not a writable directory.\n"; | ||||||
345 | } | ||||||
346 | 0 | for ('formtmplpath', 'resulttmplpath') { | |||||
347 | 0 | 0 | 0 | if ($args{$_} and !-f $args{$_}) { | |||
348 | 0 | $error .= "Argument '$_': Can't find the file $args{$_}\n"; | |||||
349 | } | ||||||
350 | } | ||||||
351 | { | ||||||
352 | 0 | 0 | local $SIG{__WARN__} = sub { die $_[0] }; | ||||
0 | |||||||
0 | |||||||
353 | 0 | eval { $args{spamfilter} = qr($args{spamfilter}) }; | |||||
0 | |||||||
354 | 0 | 0 | if ( $@ ) { | ||||
355 | 0 | my $mod_path = $INC{'CGI/ContactForm.pm'}; | |||||
356 | 0 | $@ =~ s/ at $mod_path.+//; | |||||
357 | 0 | $error .= "Argument 'spamfilter': " . escapeHTML(my $err = $@); | |||||
358 | } | ||||||
359 | } | ||||||
360 | |||||||
361 | 0 | 0 | CFdie("$error" . <<'EXAMPLE' |
||||
362 | |||||||
363 | Example: | ||||||
364 | |||||||
365 | contactform ( | ||||||
366 | recname => 'John Smith', | ||||||
367 | recmail => 'john.smith@example.com', | ||||||
368 | ); | ||||||
369 | EXAMPLE | ||||||
370 | |||||||
371 | ) if $error; | ||||||
372 | |||||||
373 | 0 | \%args; | |||||
374 | } | ||||||
375 | |||||||
376 | sub formdata { | ||||||
377 | 0 | 0 | 0 | my $max = shift; | |||
378 | 0 | 0 | if ($ENV{CONTENT_LENGTH} > 1024 * $max) { | ||||
379 | 0 | CFdie("The message size exceeds the $max KiB limit.\n" | |||||
380 | . ' Back'); |
||||||
381 | } | ||||||
382 | |||||||
383 | # create hash reference to the form data | ||||||
384 | 0 | my $in = new CGI->Vars; | |||||
385 | |||||||
386 | # trim whitespace in message headers | ||||||
387 | 0 | for (qw/name email subject/) { | |||||
388 | 0 | $in->{$_} =~ s/^\s+//; | |||||
389 | 0 | $in->{$_} =~ s/\s+$//; | |||||
390 | 0 | $in->{$_} =~ s/\s+/ /g; | |||||
391 | } | ||||||
392 | |||||||
393 | 0 | $in; | |||||
394 | } | ||||||
395 | |||||||
396 | sub formcheck { | ||||||
397 | 0 | 0 | 0 | my ($in, $defaultsubject, $error) = @_; | |||
398 | 0 | 0 | for (qw/name message/) { $error->{$_} = ' class="error"' unless $in->{$_} } | ||||
0 | |||||||
399 | 0 | 0 | 0 | $error->{subject} = ' class="error"' unless $in->{subject} or $defaultsubject; | |||
400 | 0 | 0 | $error->{email} = ' class="error"' if emailsyntax( $in->{email} ); | ||||
401 | 0 | 0 | %$error ? 1 : 0; | ||||
402 | } | ||||||
403 | |||||||
404 | sub emailsyntax { | ||||||
405 | 0 | 0 | 0 | 0 | return 1 unless my ($localpart, $domain) = shift =~ /^(.+)@(.+)/; | ||
406 | 0 | my $atom = '[^[:cntrl:] "(),.:;<>@\[\\\\\]]+'; | |||||
407 | 0 | my $qstring = '"(?:\\\\.|[^"\\\\\s]|[ \t])*"'; | |||||
408 | 0 | my $word = qr($atom|$qstring); | |||||
409 | 0 | 0 | return 1 unless $localpart =~ /^$word(?:\.$word)*$/; | ||||
410 | 0 | 0 | $domain =~ /^$atom(?:\.$atom)+$/ ? 0 : 1; | ||||
411 | } | ||||||
412 | |||||||
413 | sub mailsend { | ||||||
414 | 0 | 0 | 0 | my ($args, $in, $host) = @_; | |||
415 | |||||||
416 | # Extra headers | ||||||
417 | 0 | my @extras = "X-Originating-IP: [$host]"; | |||||
418 | 0 | 0 | if ( my $agent = $ENV{'HTTP_USER_AGENT'} ) { | ||||
419 | 0 | my @lines; | |||||
420 | 0 | while ( $agent =~ /(.{1,66})(?:\s+|$)/g ) { | |||||
421 | 0 | push @lines, $1; | |||||
422 | } | ||||||
423 | 0 | push @extras, 'User-Agent: ' . join("\r\n\t", @lines); | |||||
424 | } | ||||||
425 | 0 | 0 | push @extras, "Referer: $ENV{'HTTP_REFERER'}" if $ENV{'HTTP_REFERER'}; | ||||
426 | 0 | push @extras, "X-Mailer: CGI::ContactForm $VERSION at $ENV{HTTP_HOST}"; | |||||
427 | |||||||
428 | # Make message format=flowed (RFC 2646) | ||||||
429 | 0 | eval "use Encode 2.23 ()"; | |||||
430 | 0 | 0 | my $convert = $@ ? 0 : 1; | ||||
431 | 0 | 0 | $in->{message} = Encode::decode( $args->{encoding}, $in->{message} ) if $convert; | ||||
432 | 0 | $in->{message} = reformat( $in->{message}, { max_length => 66, opt_length => 66 } ); | |||||
433 | 0 | 0 | $in->{message} = Encode::encode( $args->{encoding}, $in->{message} ) if $convert; | ||||
434 | 0 | push @extras, "Content-type: text/plain; charset=$args->{encoding}; format=flowed"; | |||||
435 | |||||||
436 | # Send message | ||||||
437 | 0 | $Mail::Sender::NO_X_MAILER = 1; | |||||
438 | 0 | $Mail::Sender::SITE_HEADERS = join "\r\n", @extras; | |||||
439 | ref (new Mail::Sender -> MailMsg( { | ||||||
440 | smtp => $args->{smtp}, | ||||||
441 | encoding => ( $in->{message} =~ /[[:^ascii:]]/ ? 'quoted-printable' : '7bit' ), | ||||||
442 | from => ( $args->{bouncetosender} ? $in->{email} : $args->{recmail} ), | ||||||
443 | fake_from => namefix( $in->{name}, $args->{encoding} ) . " <$in->{email}>", | ||||||
444 | to => namefix( $args->{recname}, $args->{encoding} ) . " <$args->{recmail}>", | ||||||
445 | bcc => ( $args->{nocopy} ? '' : $in->{email} ), | ||||||
446 | subject => mimeencode( $in->{subject}, $args->{encoding} ), | ||||||
447 | msg => $in->{message}, | ||||||
448 | 0 | 0 | } )) or die "Cannot send mail. $Mail::Sender::Error\n"; | ||||
0 | |||||||
0 | |||||||
0 | |||||||
449 | |||||||
450 | # Print resulting page | ||||||
451 | 0 | my @resultargs = qw/recname returnlinktext returnlinkurl title thanks/; | |||||
452 | 0 | $args->{$_} = escapeHTML( $args->{$_} ) for @resultargs; | |||||
453 | my $sent_to = sprintf escapeHTML( $args->{nocopy} ? $args->{sent_to_short} : $args->{sent_to} ), | ||||||
454 | 0 | 0 | "$args->{recname}", '' . escapeHTML( $in->{email} ) . ''; | ||||
455 | 0 | $args->{returnlinkurl} =~ s/ /%20/g; | |||||
456 | 0 | 0 | if ( $args->{resulttmplpath} ) { | ||||
457 | 0 | my %result_vars; | |||||
458 | 0 | $result_vars{style} = stylesheet( $args->{styleurl} ); | |||||
459 | 0 | $result_vars{sent_to} = \$sent_to; | |||||
460 | 0 | $result_vars{$_} = \$args->{$_} for @resultargs; | |||||
461 | 0 | templateprint($args->{resulttmplpath}, $args->{encoding}, %result_vars); | |||||
462 | } else { | ||||||
463 | 0 | headprint($args); | |||||
464 | |||||||
465 | 0 | print < | |||||
466 | $args->{thanks} |
||||||
467 | $sent_to |
||||||
468 | |||||||
469 |