RT#29354: Password Security in Email [xmlhttp validation for selfservice]
[freeside.git] / fs_selfservice / FS-SelfService / cgi / signup.cgi
1 #!/usr/bin/perl -T
2 #!/usr/bin/perl -Tw
3
4 use strict;
5 use vars qw( @payby $cgi $init_data
6              $self_url $error $agentnum
7
8              $ieak_file $ieak_template
9              $signup_html $signup_template
10              $success_html $success_template
11              $collect_html $collect_template
12              $decline_html $decline_template
13            );
14
15 use subs qw( print_form print_okay print_decline
16              success_default collect_default decline_default
17            );
18 use CGI;
19 #use CGI::Carp qw(fatalsToBrowser);
20 use Tie::IxHash;
21 use Text::Template;
22 use Business::CreditCard;
23 use HTTP::BrowserDetect;
24 use HTML::Widgets::SelectLayers;
25 use FS::SelfService qw( signup_info new_customer );
26
27 #acceptable payment methods
28 #
29 #@payby = qw( CARD BILL COMP );
30 #@payby = qw( CARD BILL );
31 #@payby = qw( CARD );
32 @payby = qw( CARD PREPAY );
33
34 $ieak_file = '/usr/local/freeside/ieak.template';
35 $signup_html = -e 'signup.html'
36                  ? 'signup.html'
37                  : '/usr/local/freeside/signup.html';
38 $success_html = -e 'success.html'
39                   ? 'success.html'
40                   : '/usr/local/freeside/success.html';
41 $collect_html = -e 'collect.html'
42                   ? 'collect.html'
43                   : '/usr/local/freeside/collect.html';
44 $decline_html = -e 'decline.html'
45                   ? 'decline.html'
46                   : '/usr/local/freeside/decline.html';
47
48
49 if ( -e $ieak_file ) {
50   my $ieak_txt = Text::Template::_load_text($ieak_file)
51     or die $Text::Template::ERROR;
52   $ieak_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
53   $ieak_txt = $1;
54   $ieak_txt =~ s/\r//g; # don't double \r on old templates
55   $ieak_txt =~ s/\n/\r\n/g;
56   $ieak_template = new Text::Template ( TYPE => 'STRING', SOURCE => $ieak_txt )
57     or die $Text::Template::ERROR;
58 } else {
59   $ieak_template = '';
60 }
61
62 $agentnum = '';
63 if ( -e $signup_html ) {
64   my $signup_txt = Text::Template::_load_text($signup_html)
65     or die $Text::Template::ERROR;
66   $signup_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
67   $signup_txt = $1;
68   $signup_template = new Text::Template ( TYPE => 'STRING',
69                                           SOURCE => $signup_txt,
70                                           DELIMITERS => [ '<%=', '%>' ]
71                                         )
72     or die $Text::Template::ERROR;
73   if ( $signup_txt =~
74          /<\s*INPUT TYPE="?hidden"?\s+NAME="?agentnum"?\s+VALUE="?(\d+)"?\s*\/?\s*>/si
75   ) {
76     $agentnum = $1;
77   }
78 } else {
79   #too much maintenance hassle to keep in this file
80   die "can't find ./signup.html or /usr/local/freeside/signup.html";
81   #$signup_template = new Text::Template ( TYPE => 'STRING',
82   #                                        SOURCE => &signup_default,
83   #                                        DELIMITERS => [ '<%=', '%>' ]
84   #                                      )
85   #  or die $Text::Template::ERROR;
86 }
87
88 if ( -e $success_html ) {
89   my $success_txt = Text::Template::_load_text($success_html)
90     or die $Text::Template::ERROR;
91   $success_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
92   $success_txt = $1;
93   $success_template = new Text::Template ( TYPE => 'STRING',
94                                            SOURCE => $success_txt,
95                                            DELIMITERS => [ '<%=', '%>' ],
96                                          )
97     or die $Text::Template::ERROR;
98 } else {
99   $success_template = new Text::Template ( TYPE => 'STRING',
100                                            SOURCE => &success_default,
101                                            DELIMITERS => [ '<%=', '%>' ],
102                                          )
103     or die $Text::Template::ERROR;
104 }
105
106 if ( -e $collect_html ) {
107   my $collect_txt = Text::Template::_load_text($collect_html)
108     or die $Text::Template::ERROR;
109   $collect_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
110   $collect_txt = $1;
111   $collect_template = new Text::Template ( TYPE => 'STRING',
112                                            SOURCE => $collect_txt,
113                                            DELIMITERS => [ '<%=', '%>' ],
114                                          )
115     or die $Text::Template::ERROR;
116 } else {
117   $collect_template = new Text::Template ( TYPE => 'STRING',
118                                            SOURCE => &collect_default,
119                                            DELIMITERS => [ '<%=', '%>' ],
120                                          )
121     or die $Text::Template::ERROR;
122 }
123
124 if ( -e $decline_html ) {
125   my $decline_txt = Text::Template::_load_text($decline_html)
126     or die $Text::Template::ERROR;
127   $decline_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
128   $decline_txt = $1;
129   $decline_template = new Text::Template ( TYPE => 'STRING',
130                                            SOURCE => $decline_txt,
131                                            DELIMITERS => [ '<%=', '%>' ],
132                                          )
133     or die $Text::Template::ERROR;
134 } else {
135   $decline_template = new Text::Template ( TYPE => 'STRING',
136                                            SOURCE => &decline_default,
137                                            DELIMITERS => [ '<%=', '%>' ],
138                                          )
139     or die $Text::Template::ERROR;
140 }
141
142 $cgi = new CGI;
143
144 $init_data = signup_info( 'agentnum'   => $agentnum || scalar($cgi->param('agentnum')),
145                           'promo_code' => scalar($cgi->param('promo_code')),
146                           'reg_code'   => uc(scalar($cgi->param('reg_code'))),
147                         );
148
149 my $magic  = $cgi->param('magic') || '';
150 my $action = $cgi->param('action') || '';
151
152 if ( $magic eq 'process' || $action eq 'process_signup' ) {
153
154     $error = '';
155
156     $cgi->param('agentnum', $agentnum) if $agentnum;
157     $cgi->param('reg_code', uc(scalar($cgi->param('reg_code'))) );
158
159     #false laziness w/agent.cgi, identical except for agentnum
160     my $payby = $cgi->param('payby');
161     if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
162       #$payinfo = join('@', map { $cgi->param( $payby. "_payinfo$_" ) } (1,2) );
163       $cgi->param('payinfo' => scalar($cgi->param($payby. '_payinfo1')). '@'. 
164                                scalar($cgi->param($payby. '_payinfo2'))
165                  );
166     } else {
167       $cgi->param('payinfo' => scalar($cgi->param( $payby. '_payinfo' ) ) );
168     }
169     $cgi->param('paydate' => scalar($cgi->param( $payby. '_month' )). '-'.
170                              scalar($cgi->param( $payby. '_year' ))
171                );
172     $cgi->param('payname' => scalar($cgi->param( $payby. '_payname' ) ) );
173     $cgi->param('paycvv' => defined $cgi->param( $payby. '_paycvv' )
174                               ? scalar($cgi->param( $payby. '_paycvv' ))
175                               : ''
176                );
177     $cgi->param('paytype' => defined $cgi->param( $payby. '_paytype' )
178                               ? scalar($cgi->param( $payby. '_paytype' ))
179                               : ''
180                );
181     $cgi->param('paystate' => defined $cgi->param( $payby. '_paystate' )
182                               ? scalar($cgi->param( $payby. '_paystate' ))
183                               : ''
184                );
185
186     if ( $cgi->param('invoicing_list') ) {
187       $cgi->param('invoicing_list' => scalar($cgi->param('invoicing_list')). ', POST')
188         if $cgi->param('invoicing_list_POST');
189     } else {
190       $cgi->param('invoicing_list' => 'POST' );
191     }
192
193     #if ( $svc_x eq 'svc_acct' ) {
194     if ( $cgi->param('_password') ne $cgi->param('_password2') ) {
195       $error = $init_data->{msgcat}{passwords_dont_match}; #msgcat
196       $cgi->param('_password', '');
197       $cgi->param('_password2', '');
198     }
199
200     if ( $payby =~ /^(CARD|DCRD)$/ && $cgi->param('CARD_type') ) {
201       my $payinfo = $cgi->param('payinfo');
202       $payinfo =~ s/\D//g;
203
204       $payinfo =~ /^(\d{13,16}|\d{8,9})$/
205         or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
206       $payinfo = $1;
207       validate($payinfo)
208         or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
209       cardtype($payinfo) eq $cgi->param('CARD_type')
210         or $error ||= $init_data->{msgcat}{not_a}. $cgi->param('CARD_type');
211
212       $error ||= 'CVV2 is required'
213         if ! $cgi->param('paycvv')
214         && $init_data->{require_cvv};
215
216     }
217
218     if ($init_data->{emailinvoiceonly} && (length $cgi->param('invoicing_list') < 1)) {
219         $error ||= $init_data->{msgcat}{illegal_or_empty_text};
220     }
221
222     my $rv = '';
223     unless ( $error ) {
224       $rv = new_customer( {
225         ( map { $_ => scalar($cgi->param($_)) }
226             qw( last first ss company
227                 address1 address2 city county state zip country
228                 daytime night fax stateid stateid_state
229
230                 ship_last ship_first ship_company
231                 ship_address1 ship_address2 ship_city ship_county ship_state
232                   ship_zip ship_country
233                 ship_daytime ship_night ship_fax
234
235                 payby payinfo paycvv paydate payname paystate paytype
236                 invoicing_list referral_custnum promo_code reg_code
237                 override_ban_warn
238                 pkgpart refnum agentnum
239                 username sec_phrase _password popnum domsvc
240                 mac_addr
241                 countrycode phonenum sip_password pin prepaid_shortform
242               ),
243             grep { /^(snarf_|tax_)/ } $cgi->param
244         ),
245         'payip' => $cgi->remote_host(),
246       } );
247       $error = $rv->{'error'};
248     }
249     #eslaf
250     
251     if ( $error eq '_decline' ) {
252       print_decline();
253     } elsif ( $error eq '_collect' ) {
254       map { $cgi->param($_, $rv->{$_}) }
255         qw( popup_url reference amount );
256       print_collect($rv);
257     } elsif ( $error ) {
258       #fudge the snarf and tax info
259       no strict 'refs';
260       ${$_} = $cgi->param($_) foreach grep { /^(snarf_|tax_)/ } $cgi->param;
261
262       if ( $error =~ /^_duplicate_(card|ach)/ ) {
263         my $what = ($1 eq 'card') ? 'Credit card' : 'Electronic check';
264         $error = "Warning: $what already used to sign up recently";
265         $init_data->{'override_ban_warn'} = 1;
266       }
267
268       print_form();
269
270     } else {
271       print_okay(
272         'pkgpart' => scalar($cgi->param('pkgpart')),
273         %$rv,
274       );
275     }
276
277 } elsif ( $magic eq 'success' || $action eq 'success' ) {
278
279   $cgi->param('username', 'username');  #hmmm temp kludge
280   $cgi->param('_password', 'password');
281   print_okay( map { /^([\w ]+)$/ ? ( $_ => $1 ) : () } $cgi->param ); #hmmm
282
283 } elsif ( $magic eq 'decline' || $action eq 'decline' ) {
284
285   print_decline();
286
287 } else {
288   $error = '';
289   print_form;
290 }
291
292 sub print_form {
293
294   $error = "Error: $error" if $error && $error !~ /^Warning:/i;
295
296   my $r = {
297     $cgi->Vars,
298     %{$init_data},
299     'error' => $error,
300   };
301
302   $r->{pkgpart} ||= $r->{default_pkgpart};
303
304   $r->{referral_custnum} = $r->{'ref'};
305   #$cgi->delete('ref');
306   #$cgi->delete('init_popstate');
307   $r->{self_url} = $cgi->self_url;
308
309   $r->{prepaid_shortform} = $cgi->param('prepaid_shortform');
310
311   print $cgi->header( '-expires' => 'now' ),
312         $signup_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi',
313                                    HASH    => $r
314                                  );
315 }
316
317 sub print_collect {
318
319   $error = "Error: $error" if $error;
320
321   my $rv = shift || {};
322   my $r = {
323     $cgi->Vars,
324     %{$init_data},
325     %$rv,
326     'error' => $error,
327   };
328
329   $r->{pkgpart} ||= $r->{default_pkgpart};
330
331   $r->{referral_custnum} = $r->{'ref'};
332   $r->{self_url} = $cgi->self_url;
333
334   print $cgi->header( '-expires' => 'now' ),
335
336         $collect_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi',
337                                     HASH    => $r
338                                   );
339 }
340
341 sub print_decline {
342   my $r = {
343     %{$init_data},
344   };
345
346   print $cgi->header( '-expires' => 'now' ),
347         $decline_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi',
348                                     HASH    => $r
349                                   );
350 }
351
352 sub print_okay {
353   my %param = @_;
354   my $user_agent = new HTTP::BrowserDetect $ENV{HTTP_USER_AGENT};
355
356   my( $username, $password ) = ( '', '' );
357   my( $countrycode, $phonenum, $sip_password, $pin ) = ( '', '', '', '' );
358
359   my $svc_x = $param{signup_service} || 'svc_acct'; #just in case
360   if ( $svc_x eq 'svc_acct' ) {
361
362     $cgi->param('username') =~ /^(.+)$/
363       or die "fatal: invalid username got past FS::SelfService::new_customer";
364     $username = $1;
365     $cgi->param('_password') =~ /^(.+)$/
366       or die "fatal: invalid password got past FS::SelfService::new_customer";
367     $password = $1;
368
369   } elsif ( $svc_x eq 'svc_phone' ) {
370
371     $countrycode  = $param{countrycode};
372     $phonenum     = $param{phonenum};
373     $sip_password = $param{sip_password};
374     $pin          = $param{pin};
375
376   } else {
377     die "unknown signup service $svc_x";
378   }
379
380   ( $cgi->param('first'). ' '. $cgi->param('last') ) =~ /^(.*)$/
381     or die "fatal: invalid email_name got past FS::SelfService::new_customer";
382   my $email_name = $1; #global for template
383
384   #my %pop = ();
385   my %popnum2pop = ();
386   foreach ( @{ $init_data->{'svc_acct_pop'} } ) {
387     #push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
388     $popnum2pop{$_->{popnum}} = $_;
389   }
390
391   my( $ac, $exch, $loc);
392   my $pop = $popnum2pop{$cgi->param('popnum')};
393     #or die "fatal: invalid popnum got past FS::SelfService::new_customer";
394   if ( $pop ) {
395     ( $ac, $exch, $loc ) = ( $pop->{'ac'}, $pop->{'exch'}, $pop->{'loc'} );
396   } else {
397     ( $ac, $exch, $loc ) = ( '', '', ''); #presumably you're not using them.
398   }
399
400   #global for template
401   my $part_pkg = ( grep { $_->{'pkgpart'} eq $param{'pkgpart'} }
402                         @{ $init_data->{'part_pkg'} }
403                  )[0];
404   my $pkg =  $part_pkg->{'pkg'};
405
406   if ( $ieak_template && $user_agent->windows && $user_agent->ie ) {
407
408     #send an IEAK config
409     print $cgi->header('application/x-Internet-signup'),
410           $ieak_template->fill_in();
411
412   } else { #send a simple confirmation
413
414     print $cgi->header( '-expires' => 'now' ),
415           $success_template->fill_in( HASH => {
416
417             %{$init_data},
418
419             email_name     => $email_name,
420             pkg            => $pkg,
421             part_pkg       => \$part_pkg,
422
423             signup_service => $svc_x,
424
425             #for svc_acct
426             username       => $username,
427             password       => $password,
428             _password      => $password,
429             ac             => $ac,   #for dialup POP
430             exch           => $exch, #
431             loc            => $loc,  #
432
433             #for svc_phone
434             countrycode    => $countrycode,
435             phonenum       => $phonenum,
436             sip_password   => $sip_password,
437             pin            => $pin,
438
439           });
440   }
441
442 }
443
444 sub success_default { #html to use if you don't specify a success file
445   <<'END';
446 <HTML><HEAD><TITLE>Signup successful</TITLE></HEAD>
447 <BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Signup successful</FONT><BR><BR>
448 Thanks for signing up!
449 <BR><BR>
450 Signup information for <%= $email_name %>:
451 <BR><BR>
452 Username: <%= $username %><BR>
453 Password: <%= $password %><BR>
454 Access number: (<%= $ac %>) / <%= $exch %> - <%= $local %><BR>
455 Package: <%= $pkg %><BR>
456 </BODY></HTML>
457 END
458 }
459
460 sub collect_default { #html to use if there is a collect phase
461   <<'END';
462 <HTML><HEAD><TITLE>Pay now</TITLE></HEAD>
463 <BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Pay now</FONT><BR><BR>
464 <%=
465 #<SCRIPT TYPE="text/javascript">
466 #  function popcollect() {
467 #    overlib( OLiframeContent('<%= $popup_url %>', 336, 550, 'Secure Payment Area', 0, 'auto' ), CAPTION, 'Pay now', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK, BGCOLOR, '#333399', CGCOLOR, '#333399', CLOSETEXT, 'Close' );
468 #    return false;
469 #  }
470 #</SCRIPT>
471 #<SCRIPT TYPE="text/javascript" SRC="overlibmws.js"></SCRIPT>
472 #<SCRIPT TYPE="text/javascript" SRC="overlibmws_iframe.js"></SCRIPT>
473 #<SCRIPT TYPE="text/javascript" SRC="overlibmws_draggable.js"></SCRIPT>
474 #<SCRIPT TYPE="text/javascript" SRC="overlibmws_crossframe.js"></SCRIPT>
475 #<SCRIPT TYPE="text/javascript" SRC="iframecontentmws.js"></SCRIPT>
476 %>
477 You are about to contact our payment processor to pay <%= $amount %> for
478 <%= $pkg %>.<BR><BR>
479 Your transaction reference number is <%= $reference %><BR><BR>
480 <FORM NAME="collect_popper" method="post" action="<%= $popup_url %>">
481 <%=
482   my %itemhash = @collectitems ;
483   foreach my $input (keys %itemhash) {
484     $OUT .= qq!<INPUT NAME="$input" TYPE="hidden" VALUE="$itemhash{$input}">!;
485   }
486 %>
487 <INPUT NAME="submit" type="submit" value="Pay now">
488 </FORM>
489 </BODY></HTML>
490 END
491 }
492
493 sub decline_default { #html to use if there is a decline
494   <<'END';
495 <HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
496 <BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Processing error</FONT><BR><BR>
497 There has been an error processing your account.  Please contact customer
498 support.
499 </BODY></HTML>
500 END
501 }
502
503 # subs for the templates...
504
505 package FS::SelfService::_signupcgi;
506 use HTML::Entities;
507 use FS::SelfService qw( regionselector expselect popselector domainselector
508                         didselector
509                       );
510
511 sub add_password_validation {
512   my $fieldid = shift;
513   my $out = '';
514   if ((-e './send_xmlhttp.html') && (-e './add_password_validation.html')) {
515     my $template = new Text::Template( TYPE   => 'FILE',
516                                        SOURCE => "./send_xmlhttp.html",
517                                        DELIMITERS => [ '<%=', '%>' ],
518                                        UNTAINT => 1,                   
519                                      )
520       or die $Text::Template::ERROR;
521     $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
522     $template = new Text::Template( TYPE   => 'FILE',
523                                        SOURCE => "./add_password_validation.html",
524                                        DELIMITERS => [ '<%=', '%>' ],
525                                        UNTAINT => 1,                   
526                                      )
527       or die $Text::Template::ERROR;
528     $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
529     $out .= <<ENDOUT;
530 <SCRIPT>
531 add_password_validation('$fieldid');
532 </SCRIPT>
533 ENDOUT
534   }
535   return $out;
536 }
537
538