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.50'; | ||||||
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 DEPENDENCY | ||||||
198 | |||||||
199 | C |
||||||
200 | L |
||||||
201 | manually, you shall create C and upload | ||||||
202 | C |
||||||
203 | |||||||
204 | =head1 AUTHENTICATION | ||||||
205 | |||||||
206 | If you have access to a mail server that is configured to automatically | ||||||
207 | accept sending messages from a CGI script to any address, you don't need | ||||||
208 | to worry about authentication. Otherwise you need to somehow authenticate | ||||||
209 | to the server, for instance by adding something like this right after the | ||||||
210 | C | ||||||
211 | |||||||
212 | %Mail::Sender::default = ( | ||||||
213 | auth => 'LOGIN', | ||||||
214 | authid => 'username', | ||||||
215 | authpwd => 'password', | ||||||
216 | ); | ||||||
217 | |||||||
218 | C |
||||||
219 | and C |
||||||
220 | find out which protocol and username/password pair to use. | ||||||
221 | |||||||
222 | If there are multiple forms, a more convenient way to deal with a need | ||||||
223 | for authentication may be to make use of the C |
||||||
224 | is included in the distribution. You just edit it and upload it to the | ||||||
225 | same directory as the one where C |
||||||
226 | |||||||
227 | See the L |
||||||
228 | |||||||
229 | =head1 AUTHOR, COPYRIGHT AND LICENSE | ||||||
230 | |||||||
231 | Copyright (c) 2003-2009 Gunnar Hjalmarsson | ||||||
232 | http://www.gunnar.cc/cgi-bin/contact.pl | ||||||
233 | |||||||
234 | This module is free software; you can redistribute it and/or modify it | ||||||
235 | under the same terms as Perl itself. | ||||||
236 | |||||||
237 | =head1 SEE ALSO | ||||||
238 | |||||||
239 | L |
||||||
240 | L |
||||||
241 | |||||||
242 | =cut | ||||||
243 | |||||||
244 | 1 | 1 | 4487 | use strict; | |||
1 | 2 | ||||||
1 | 25 | ||||||
245 | 1 | 1 | 706 | use CGI 'escapeHTML'; | |||
1 | 26851 | ||||||
1 | 5 | ||||||
246 | 1 | 1 | 113 | use File::Basename; | |||
1 | 2 | ||||||
1 | 99 | ||||||
247 | 1 | 1 | 6 | use File::Spec; | |||
1 | 2 | ||||||
1 | 19 | ||||||
248 | 1 | 1 | 4 | use Fcntl qw(:DEFAULT :flock); | |||
1 | 1 | ||||||
1 | 311 | ||||||
249 | 1 | 1 | 6 | use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); | |||
1 | 1 | ||||||
1 | 57 | ||||||
250 | 1 | 1 | 6 | use Exporter; | |||
1 | 1 | ||||||
1 | 132 | ||||||
251 | @ISA = 'Exporter'; | ||||||
252 | @EXPORT = 'contactform'; | ||||||
253 | @EXPORT_OK = 'CFdie'; | ||||||
254 | |||||||
255 | BEGIN { | ||||||
256 | sub CFdie($) { | ||||||
257 | 0 | 0 | 0 | print "Status: 400 Bad Request\n"; | |||
258 | 0 | print "Content-type: text/html\n\nError\n", shift; |
|||||
259 | 0 | 0 | if ( $ENV{MOD_PERL} ) { | ||||
260 | 0 | 0 | if ( $] < 5.006 ) { | ||||
261 | 0 | require Apache; | |||||
262 | 0 | Apache::exit(); | |||||
263 | } | ||||||
264 | } | ||||||
265 | 0 | exit 1; | |||||
266 | } | ||||||
267 | |||||||
268 | 1 | 1 | 63 | eval "use Mail::Sender"; | |||
1 | 1 | 790 | |||||
1 | 98112 | ||||||
1 | 37 | ||||||
269 | 1 | 50 | 3602 | CFdie($@) if $@; | |||
270 | } | ||||||
271 | |||||||
272 | sub contactform { | ||||||
273 | 0 | 0 | 0 | local $^W = 1; # enables warnings | |||
274 | 0 | my ($error, $in) = {}; | |||||
275 | 0 | my $time = time; | |||||
276 | 0 | 0 | my $host = $ENV{'REMOTE_ADDR'} or die "REMOTE_ADDR not set\n"; | ||||
277 | 0 | umask 0; | |||||
278 | 0 | my $args = &arguments; | |||||
279 | 0 | 0 | if ($ENV{REQUEST_METHOD} eq 'POST') { | ||||
280 | 0 | checktimestamp( $args->{tempdir}, $time ); | |||||
281 | 0 | $in = formdata( $args->{maxsize} ); | |||||
282 | 0 | 0 | if (formcheck($in, $args->{subject}, $error) == 0) { | ||||
283 | 0 | checkspamfilter( $in->{message}, $args->{spamfilter} ); | |||||
284 | 0 | checkmaxperhour($args, $time, $host); | |||||
285 | 0 | eval { mailsend($args, $in, $host) }; | |||||
0 | |||||||
286 | 0 | 0 | CFdie( escapeHTML(my $msg = $@) ) if $@; | ||||
287 | 0 | return; | |||||
288 | } | ||||||
289 | } else { | ||||||
290 | 0 | settimestamp( $args->{tempdir}, $time ); | |||||
291 | } | ||||||
292 | 0 | formprint($args, $in, $error); | |||||
293 | } | ||||||
294 | |||||||
295 | sub arguments { | ||||||
296 | 0 | 0 | 0 | my %defaults = ( | |||
297 | recname => '', | ||||||
298 | recmail => '', | ||||||
299 | smtp => 'localhost', | ||||||
300 | styleurl => '', | ||||||
301 | returnlinktext => 'Main Page', | ||||||
302 | returnlinkurl => '/', | ||||||
303 | subject => '', | ||||||
304 | nocopy => 0, | ||||||
305 | bouncetosender => 0, | ||||||
306 | formtmplpath => '', | ||||||
307 | resulttmplpath => '', | ||||||
308 | maxsize => 100, | ||||||
309 | maxperhour => 5, | ||||||
310 | tempdir => '', | ||||||
311 | spamfilter => '(?is:|\[/url]|https?:/(?:.+https?:/){3})', | ||||||
312 | title => 'Send email to', | ||||||
313 | namelabel => 'Your name:', | ||||||
314 | emaillabel => 'Your email:', | ||||||
315 | subjectlabel => 'Subject:', | ||||||
316 | msglabel => 'Message:', | ||||||
317 | reset => 'Reset', | ||||||
318 | send => 'Send', | ||||||
319 | erroralert => 'Fields with %s need to be filled or corrected.', | ||||||
320 | marked => 'marked labels', | ||||||
321 | thanks => 'Thanks for your message!', | ||||||
322 | sent_to => 'The message was sent to %s with a copy to %s.', | ||||||
323 | sent_to_short => 'The message was sent to %s.', | ||||||
324 | encoding => 'ISO-8859-1', | ||||||
325 | ); | ||||||
326 | 0 | my $error; | |||||
327 | 0 | 0 | if ( @_ % 2 ) { | ||||
328 | 0 | $error .= "Odd number of elements in argument list:\n" | |||||
329 | . " The contactform() function expects a number of key/value pairs.\n"; | ||||||
330 | } | ||||||
331 | 0 | my %args = ( %defaults, @_ ); | |||||
332 | 0 | for (qw/recname recmail/) { | |||||
333 | 0 | 0 | $error .= "The compulsory argument '$_' is missing.\n" unless $args{$_}; | ||||
334 | } | ||||||
335 | 0 | for (keys %args) { | |||||
336 | 0 | 0 | $error .= "Unknown argument: '$_'\n" unless defined $defaults{$_}; | ||||
337 | } | ||||||
338 | 0 | 0 | 0 | if ($args{recmail} and emailsyntax($args{recmail})) { | |||
339 | 0 | $error .= "'$args{recmail}' is not a valid email address.\n"; | |||||
340 | } | ||||||
341 | 0 | 0 | 0 | unless ($args{tempdir}) { | |||
342 | 0 | 0 | 0 | unless (-d $CGITempFile::TMPDIRECTORY and -w _ and -x _) { | |||
0 | |||||||
343 | 0 | $error .= "You need to state a temporary directory via the 'tempdir' argument.\n"; | |||||
344 | } | ||||||
345 | } elsif (!(-d $args{tempdir} and -w _ and -x _)) { | ||||||
346 | $error .= "'$args{tempdir}' is not a writable directory.\n"; | ||||||
347 | } | ||||||
348 | 0 | for ('formtmplpath', 'resulttmplpath') { | |||||
349 | 0 | 0 | 0 | if ($args{$_} and !-f $args{$_}) { | |||
350 | 0 | $error .= "Argument '$_': Can't find the file $args{$_}\n"; | |||||
351 | } | ||||||
352 | } | ||||||
353 | { | ||||||
354 | 0 | 0 | local $SIG{__WARN__} = sub { die $_[0] }; | ||||
0 | |||||||
0 | |||||||
355 | 0 | eval { $args{spamfilter} = qr($args{spamfilter}) }; | |||||
0 | |||||||
356 | 0 | 0 | if ( $@ ) { | ||||
357 | 0 | my $mod_path = $INC{'CGI/ContactForm.pm'}; | |||||
358 | 0 | $@ =~ s/ at $mod_path.+//; | |||||
359 | 0 | $error .= "Argument 'spamfilter': " . escapeHTML(my $err = $@); | |||||
360 | } | ||||||
361 | } | ||||||
362 | |||||||
363 | 0 | 0 | CFdie("$error" . <<'EXAMPLE' |
||||
364 | |||||||
365 | Example: | ||||||
366 | |||||||
367 | contactform ( | ||||||
368 | recname => 'John Smith', | ||||||
369 | recmail => 'john.smith@example.com', | ||||||
370 | ); | ||||||
371 | EXAMPLE | ||||||
372 | |||||||
373 | ) if $error; | ||||||
374 | |||||||
375 | 0 | \%args; | |||||
376 | } | ||||||
377 | |||||||
378 | sub formdata { | ||||||
379 | 0 | 0 | 0 | my $max = shift; | |||
380 | 0 | 0 | if ($ENV{CONTENT_LENGTH} > 1024 * $max) { | ||||
381 | 0 | CFdie("The message size exceeds the $max KiB limit.\n" | |||||
382 | . ' Back'); |
||||||
383 | } | ||||||
384 | |||||||
385 | # create hash reference to the form data | ||||||
386 | 0 | my $in = new CGI->Vars; | |||||
387 | |||||||
388 | # trim whitespace in message headers | ||||||
389 | 0 | for (qw/name email subject/) { | |||||
390 | 0 | $in->{$_} =~ s/^\s+//; | |||||
391 | 0 | $in->{$_} =~ s/\s+$//; | |||||
392 | 0 | $in->{$_} =~ s/\s+/ /g; | |||||
393 | } | ||||||
394 | |||||||
395 | 0 | $in; | |||||
396 | } | ||||||
397 | |||||||
398 | sub formcheck { | ||||||
399 | 0 | 0 | 0 | my ($in, $defaultsubject, $error) = @_; | |||
400 | 0 | 0 | for (qw/name message/) { $error->{$_} = ' class="error"' unless $in->{$_} } | ||||
0 | |||||||
401 | 0 | 0 | 0 | $error->{subject} = ' class="error"' unless $in->{subject} or $defaultsubject; | |||
402 | 0 | 0 | $error->{email} = ' class="error"' if emailsyntax( $in->{email} ); | ||||
403 | 0 | 0 | %$error ? 1 : 0; | ||||
404 | } | ||||||
405 | |||||||
406 | sub emailsyntax { | ||||||
407 | 0 | 0 | 0 | 0 | return 1 unless my ($localpart, $domain) = shift =~ /^(.+)@(.+)/; | ||
408 | 0 | my $atom = '[^[:cntrl:] "(),.:;<>@\[\\\\\]]+'; | |||||
409 | 0 | my $qstring = '"(?:\\\\.|[^"\\\\\s]|[ \t])*"'; | |||||
410 | 0 | my $word = qr($atom|$qstring); | |||||
411 | 0 | 0 | return 1 unless $localpart =~ /^$word(?:\.$word)*$/; | ||||
412 | 0 | 0 | $domain =~ /^$atom(?:\.$atom)+$/ ? 0 : 1; | ||||
413 | } | ||||||
414 | |||||||
415 | sub mailsend { | ||||||
416 | 0 | 0 | 0 | my ($args, $in, $host) = @_; | |||
417 | |||||||
418 | # Extra headers | ||||||
419 | 0 | my @extras = "X-Originating-IP: [$host]"; | |||||
420 | 0 | 0 | if ( my $agent = $ENV{'HTTP_USER_AGENT'} ) { | ||||
421 | 0 | my @lines; | |||||
422 | 0 | while ( $agent =~ /(.{1,66})(?:\s+|$)/g ) { | |||||
423 | 0 | push @lines, $1; | |||||
424 | } | ||||||
425 | 0 | push @extras, 'User-Agent: ' . join("\r\n\t", @lines); | |||||
426 | } | ||||||
427 | 0 | 0 | push @extras, "Referer: $ENV{'HTTP_REFERER'}" if $ENV{'HTTP_REFERER'}; | ||||
428 | 0 | push @extras, "X-Mailer: CGI::ContactForm $VERSION at $ENV{HTTP_HOST}"; | |||||
429 | |||||||
430 | # Make message format=flowed (RFC 2646) | ||||||
431 | 0 | eval "use Encode 2.23 ()"; | |||||
432 | 0 | 0 | my $convert = $@ ? 0 : 1; | ||||
433 | 0 | 0 | $in->{message} = Encode::decode( $args->{encoding}, $in->{message} ) if $convert; | ||||
434 | 0 | $in->{message} = reformat( $in->{message}, { max_length => 66, opt_length => 66 } ); | |||||
435 | 0 | 0 | $in->{message} = Encode::encode( $args->{encoding}, $in->{message} ) if $convert; | ||||
436 | 0 | push @extras, "Content-type: text/plain; charset=$args->{encoding}; format=flowed"; | |||||
437 | |||||||
438 | # Send message | ||||||
439 | 0 | $Mail::Sender::NO_X_MAILER = 1; | |||||
440 | 0 | $Mail::Sender::SITE_HEADERS = join "\r\n", @extras; | |||||
441 | ref (new Mail::Sender -> MailMsg( { | ||||||
442 | smtp => $args->{smtp}, | ||||||
443 | encoding => ( $in->{message} =~ /[[:^ascii:]]/ ? 'quoted-printable' : '7bit' ), | ||||||
444 | from => ( $args->{bouncetosender} ? $in->{email} : $args->{recmail} ), | ||||||
445 | fake_from => namefix( $in->{name}, $args->{encoding} ) . " <$in->{email}>", | ||||||
446 | to => namefix( $args->{recname}, $args->{encoding} ) . " <$args->{recmail}>", | ||||||
447 | bcc => ( $args->{nocopy} ? '' : $in->{email} ), | ||||||
448 | subject => mimeencode( $in->{subject}, $args->{encoding} ), | ||||||
449 | msg => $in->{message}, | ||||||
450 | 0 | 0 | } )) or die "Cannot send mail. $Mail::Sender::Error\n"; | ||||
0 | |||||||
0 | |||||||
0 | |||||||
451 | |||||||
452 | # Print resulting page | ||||||
453 | 0 | my @resultargs = qw/recname returnlinktext returnlinkurl title thanks/; | |||||
454 | 0 | $args->{$_} = escapeHTML( $args->{$_} ) for @resultargs; | |||||
455 | my $sent_to = sprintf escapeHTML( $args->{nocopy} ? $args->{sent_to_short} : $args->{sent_to} ), | ||||||
456 | 0 | 0 | "$args->{recname}", '' . escapeHTML( $in->{email} ) . ''; | ||||
457 | 0 | $args->{returnlinkurl} =~ s/ /%20/g; | |||||
458 | 0 | 0 | if ( $args->{resulttmplpath} ) { | ||||
459 | 0 | my %result_vars; | |||||
460 | 0 | $result_vars{style} = stylesheet( $args->{styleurl} ); | |||||
461 | 0 | $result_vars{sent_to} = \$sent_to; | |||||
462 | 0 | $result_vars{$_} = \$args->{$_} for @resultargs; | |||||
463 | 0 | templateprint($args->{resulttmplpath}, $args->{encoding}, %result_vars); | |||||
464 | } else { | ||||||
465 | 0 | headprint($args); | |||||
466 | |||||||
467 | 0 | print < | |||||
468 | $args->{thanks} |
||||||
469 | $sent_to |
||||||
470 | |||||||
471 |