RT#29354: Password Security in Email [customer fields, images, js files]
[freeside.git] / fs_selfservice / FS-SelfService / cgi / selfservice.cgi
index f7fe308..aff9bca 100755 (executable)
@@ -1,10 +1,11 @@
-#!/usr/bin/perl -Tw
+#!/usr/bin/perl -w
 
 use strict;
-use vars qw($DEBUG $cgi $session_id $form_max $template_dir);
+use vars qw($DEBUG $cgi $session_id $pw_session_id $form_max $template_dir);
 use subs qw(do_template);
 use CGI;
 use CGI::Carp qw(fatalsToBrowser);
+use CGI::Cookie;
 use Text::Template;
 use HTML::Entities;
 use Date::Format;
@@ -14,12 +15,16 @@ use FS::SelfService qw(
   access_info login_info login customer_info edit_info invoice
   payment_info process_payment realtime_collect process_prepay
   list_pkgs order_pkg signup_info order_recharge
-  part_svc_info provision_acct provision_external provision_phone
+  part_svc_info provision_acct provision_external provision_phone provision_forward
   unprovision_svc change_pkg suspend_pkg domainselector
   list_svcs list_svc_usage list_cdr_usage list_support_usage
   myaccount_passwd list_invoices create_ticket get_ticket did_report
   adjust_ticket_priority
   mason_comp port_graph
+  start_thirdparty finish_thirdparty
+  reset_passwd check_reset_passwd process_reset_passwd
+  validate_passwd
+  billing_history
 );
 
 $template_dir = '.';
@@ -30,49 +35,6 @@ $form_max = 255;
 
 $cgi = new CGI;
 
-unless ( defined $cgi->param('session') ) {
-  my $login_info = login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
-
-  do_template('login', $login_info );
-  exit;
-}
-
-if ( $cgi->param('session') eq 'login' ) {
-
-  $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i
-    or die "illegal username";
-  my $username = $1;
-
-  $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/
-    or die "illegal domain";
-  my $domain = $1;
-
-  $cgi->param('password') =~ /^(.{0,$form_max})$/
-    or die "illegal password";
-  my $password = $1;
-
-  my $rv = login(
-    'username' => $username,
-    'domain'   => $domain,
-    'password' => $password,
-  );
-  if ( $rv->{error} ) {
-    my $login_info = login_info( 'agentnum' => $cgi->param('agentnum') );
-    do_template('login', {
-      'error'    => $rv->{error},
-      'username' => $username,
-      'domain'   => $domain,
-      %$login_info,
-    } );
-    exit;
-  } else {
-    $cgi->param('session' => $rv->{session_id} );
-    $cgi->param('action'  => 'myaccount' );
-  }
-}
-
-$session_id = $cgi->param('session');
-
 #order|pw_list XXX ???
 my @actions = ( qw(
   myaccount
@@ -87,6 +49,8 @@ my @actions = ( qw(
   make_term_payment
   make_thirdparty_payment
   post_thirdparty_payment
+  finish_thirdparty_payment
+  cancel_thirdparty_payment
   payment_results
   ach_payment_results
   recharge_prepay
@@ -108,6 +72,7 @@ my @actions = ( qw(
   process_svc_acct
   process_svc_phone
   process_svc_external
+  process_svc_forward
   delete_svc
   view_usage
   view_usage_details
@@ -119,11 +84,139 @@ my @actions = ( qw(
   process_change_password
   customer_suspend_pkg
   process_suspend_pkg
+  switch_cust
+  history
+  validate_password
 ));
-$cgi->param('action') =~ ( '^(' . join('|', @actions) . ')$' )
-  or die "unknown action ". $cgi->param('action');
-my $action = $1;
+
+my @nologin_actions = (qw(
+  forgot_password
+  do_forgot_password
+  process_forgot_password
+  do_process_forgot_password
+  process_forgot_password_session
+  validate_password_nologin
+));
+push @actions, @nologin_actions;
+my %nologin_actions = map { $_=>1 } @nologin_actions;
+
+my $action = 'myaccount'; # sensible default
+
+if ( $cgi->param('action') =~ /^process_forgot_password_session_(\w+)$/ ) {
+  $action = 'process_forgot_password_session';
+  $pw_session_id = $1;
+} elsif ( $cgi->param('action') =~ /^(\w+)$/ ) {
+  if (grep {$_ eq $1} @actions) {
+    $action = $1;
+  } else {
+    warn "WARNING: unrecognized action '$1'\n";
+  }
+}
+unless ( $nologin_actions{$action} ) {
+
+  my %cookies = CGI::Cookie->fetch;
+
+  my $login_rv = {};
+
+  if ( exists($cookies{'session'}) ) {
+
+    $session_id = $cookies{'session'}->value;
+
+    if ( $session_id eq 'login' ) {
+      # then we've just come back from the login page
+
+      $cgi->param('password') =~ /^(.{0,$form_max})$/;
+      my $password = $1;
+
+      if ( $cgi->param('email') =~ /^\s*([a-z0-9_\-\.\@]{1,$form_max})\s*$/i ) {
+
+        my $email = $1;
+        $login_rv = login(
+          'email'    => $email,
+          'password' => $password
+        );
+
+       if ( $login_rv->{'error'} ) {
+         my $ip = $cgi->remote_addr();
+         warn("login failure [email $email] [ip $ip] [error $login_rv->{error}]");
+       } else {
+         #successful login
+       }
+
+       $session_id = $login_rv->{'session_id'};
+
+      } else {
+
+        $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i;
+        my $username = $1;
+
+        $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/;
+        my $domain = $1;
+
+        if ( $username and $domain and $password ) {
+
+          # authenticate
+          $login_rv = login(
+            'username' => $username,
+            'domain'   => $domain,
+            'password' => $password,
+          );
+          $session_id = $login_rv->{'session_id'};
+
+        } elsif ( $username or $domain or $password ) {
+        
+          my $error = 'Illegal '; #XXX localization...
+          my $count = 0;
+          if ( !$username ) {
+            $error .= 'username';
+            $count++;
+          }
+          if ( !$domain )  {
+            $error .= ', ' if $count;
+            $error .= 'domain';
+            $count++;
+          }
+          if ( !$password ) {
+            $error .= ', ' if $count;
+            $error .= 'and ' if $count > 1;
+            $error .= 'password';
+            $count++;
+          }
+          $error .= '.';
+          $login_rv = {
+            'username'  => $username,
+            'domain'    => $domain,
+            'password'  => $password,
+            'error'     => $error,
+          };
+          $session_id = undef; # attempt login again
+
+        }
+
+      } # else there was no input, so show no error message
+
+    } # else session_id ne 'login'
+
+  } # else there is no session cookie
+
+  if ( !$session_id ) {
+    # show the login page
+    $session_id = 'login'; # set state
+    my $login_info = login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
+
+    do_template('login', { %$login_rv, %$login_info });
+    exit;
+  }
+
+  # at this point $session_id is a real session
+
+  if ( ! $login_rv->{'custnum'} && ! $login_rv->{'svcnum'} && $login_rv->{'customers'} ) {
+    #select a customer if we're a multi-contact customer
+    do_template('select_cust', { %$login_rv } );
+    exit;
+  }
+
+}
 
 warn "calling $action sub\n"
   if $DEBUG;
@@ -131,12 +224,14 @@ $FS::SelfService::DEBUG = $DEBUG;
 my $result = eval "&$action();";
 die $@ if $@;
 
+use Data::Dumper;
 warn Dumper($result) if $DEBUG;
 
 if ( $result->{error} && ( $result->{error} eq "Can't resume session"
   || $result->{error} eq "Expired session") ) { #ick
 
-  my $login_info = login_info();
+  $session_id = 'login';
+  my $login_info = login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
   do_template('login', $login_info);
   exit;
 }
@@ -155,7 +250,13 @@ do_template($action, {
 
 #--
 
-use Data::Dumper;
+sub switch_cust {
+  $action = 'myaccount';
+  FS::SelfService::switch_cust( 'session_id' => $session_id,
+                                'custnum'    => scalar($cgi->param('custnum')),
+                              );
+}
+
 sub myaccount { 
   customer_info( 'session_id' => $session_id ); 
 }
@@ -237,6 +338,7 @@ sub process_change_pay {
             'error' => '<FONT COLOR="#FF0000">Postal or email required.</FONT>',
           };
         }
+
         _process_change_info( 'change_pay', @list );
 }
 
@@ -255,6 +357,10 @@ sub invoices {
   list_invoices( 'session_id' => $session_id, );
 }
 
+sub history {
+  billing_history( 'session_id' => $session_id, );
+}
+
 sub tktcreate {
   my $customer_info = customer_info( 'session_id' => $session_id );
   return $customer_info if ( $customer_info->{'error'} );
@@ -479,10 +585,15 @@ sub make_payment {
 
   my $payment_info = payment_info( 'session_id' => $session_id );
 
+  my $amount = 
+    ($payment_info->{'balance'} && ($payment_info->{'balance'} > 0))
+    ? $payment_info->{'balance'}
+    : '';
+
   my $tr_amount_fee = mason_comp(
     'session_id' => $session_id,
     'comp'       => '/elements/tr-amount_fee.html',
-    'args'       => [ 'amount' => $payment_info->{'balance'},
+    'args'       => [ 'amount' => $amount,
                     ],
   );
 
@@ -558,7 +669,10 @@ sub payment_results {
   my $auto = 0;
   $auto = 1 if $cgi->param('auto');
 
-  $cgi->param('paybatch') =~ /^([\w\-\.]+)$/ or die "illegal paybatch";
+  $cgi->param('payunique') =~ /^([\w\-\.]*)$/ or die "illegal payunique";
+  my $payunique = $1;
+
+  $cgi->param('paybatch') =~ /^([\w\-\.]*)$/ or die "illegal paybatch";
   my $paybatch = $1;
 
   $cgi->param('discount_term') =~ /^(\d*)$/ or die "illegal discount_term";
@@ -582,6 +696,7 @@ sub payment_results {
     'country'    => $country,
     'save'       => $save,
     'auto'       => $auto,
+    'payunique'  => $payunique,
     'paybatch'   => $paybatch,
     'discount_term' => $discount_term,
   );
@@ -663,24 +778,48 @@ sub ach_payment_results {
 }
 
 sub make_thirdparty_payment {
-  payment_info('session_id' => $session_id);
+  my $payment_info = payment_info('session_id' => $session_id);
+  $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
+    or die "illegal payby method";
+  $payment_info->{'payby_method'} = $1;
+  $payment_info->{'error'} = $cgi->param('error');
+
+  $payment_info;
 }
 
 sub post_thirdparty_payment {
-  $cgi->param('payby_method') =~ /^(CC|ECHECK)$/
+  $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
     or die "illegal payby method";
   my $method = $1;
   $cgi->param('amount') =~ /^(\d+(\.\d*)?)$/
     or die "illegal amount";
   my $amount = $1;
-  my $result = realtime_collect( 
+  my $result = start_thirdparty(
     'session_id' => $session_id,
     'method' => $method, 
     'amount' => $amount,
   );
+  if ( $result->{error} ) {
+    $cgi->param('action', 'make_thirdparty_payment');
+    $cgi->param('error', $result->{error});
+    print $cgi->redirect( $cgi->self_url );
+    exit;
+  }
+
   $result;
 }
 
+sub finish_thirdparty_payment {
+  my %param = $cgi->Vars;
+  finish_thirdparty( 'session_id' => $session_id, %param );
+  # result contains either 'error' => error message, or the payment details
+}
+
+sub cancel_thirdparty_payment {
+  $action = 'make_thirdparty_payment';
+  finish_thirdparty( 'session_id' => $session_id, '_cancel' => 1 );
+}
+
 sub make_term_payment {
   $cgi->param('amount') =~ /^(\d+\.\d{2})$/
     or die "illegal payment amount";
@@ -736,7 +875,7 @@ sub provision_svc {
 
   my $result = part_svc_info(
     'session_id' => $session_id,
-    map { $_ => $cgi->param($_) } qw( pkgnum svcpart svcnum ),
+    map { $_ => ($cgi->param($_) || '') } qw( pkgnum svcpart svcnum ),
   );
   die $result->{'error'} if exists $result->{'error'} && $result->{'error'};
 
@@ -822,6 +961,33 @@ sub process_svc_external {
   );
 }
 
+sub process_svc_forward {
+
+  my $result = provision_forward (
+    'session_id' => $session_id,
+    map { $_ => $cgi->param($_) || '' } qw(
+      pkgnum svcpart srcsvc src dstsvc dst )
+  );
+
+  if ( exists $result->{'error'} && $result->{'error'} ) { 
+    #warn "$result $result->{'error'}"; 
+    $action = 'provision_svc_forward';
+    return {
+      $cgi->Vars,
+      %{ part_svc_info( 'session_id' => $session_id,
+                        map { $_ => $cgi->param($_) } qw( svcnum pkgnum svcpart )
+                      )
+      },
+      'error' => $result->{'error'},
+    };
+  } else {
+    #just go to setup services page, results will be visible there
+    $action = 'provision';
+    return provision();
+  }
+
+}
+
 sub delete_svc {
   unprovision_svc(
     'session_id' => $session_id,
@@ -830,11 +996,17 @@ sub delete_svc {
 }
 
 sub view_usage {
-  list_svcs(
+  my $res = list_svcs(
     'session_id'  => $session_id,
-    'svcdb'       => [ 'svc_acct', 'svc_phone', 'svc_port', ],
+    'svcdb'       => [ 'svc_acct', 'svc_phone', 'svc_port', 'svc_pbx' ],
     'ncancelled'  => 1,
   );
+  if ($res->{hide_usage}) {
+    $action = 'myaccount';
+    return myaccount();
+  } else {
+    return $res;
+  }
 }
 
 sub real_port_graph {
@@ -921,6 +1093,54 @@ sub process_change_password {
 
 }
 
+sub forgot_password {
+  login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
+}
+
+sub do_forgot_password {
+  reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( agentnum email username domain )
+  );
+}
+
+sub process_forgot_password {
+  check_reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( session_id )
+  );
+}
+
+sub process_forgot_password_session {
+  $action = 'process_forgot_password';
+  check_reset_passwd(
+    'session_id' => $pw_session_id,
+  );
+}
+
+sub do_process_forgot_password {
+  process_reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( session_id new_password new_password2 )
+  );
+}
+
+sub validate_password {
+  validate_passwd(
+    'session_id' => $session_id,
+    map { $_ => scalar($cgi->param($_)) }
+      qw( fieldid svcnum check_password )
+  )
+}
+
+sub validate_password_nologin {
+  $action = 'validate_password'; #use same landing page
+  validate_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( fieldid check_password )
+  )
+}
+
 #--
 
 sub do_template {
@@ -930,54 +1150,63 @@ sub do_template {
   $cgi->delete_all();
   $fill_in->{'selfurl'} = $cgi->self_url;
   $fill_in->{'cgi'} = \$cgi;
+  $fill_in->{'error'} = $cgi->param('error') if $cgi->param('error');
 
-  my $access_info = $session_id
+  my $access_info = ($session_id and $session_id ne 'login')
                       ? access_info( 'session_id' => $session_id )
                       : {};
   $fill_in->{$_} = $access_info->{$_} foreach keys %$access_info;
 
-  
-    if($result && ref($result) && $result->{'format'} && $result->{'content'}
-       && $result->{'format'} eq 'csv') {
-       print $cgi->header('-expires' => 'now',
-               '-Content-Type' => 'text/csv',
-               '-Content-Disposition' => "attachment;filename=output.csv",
-               ),
-           $result->{'content'};
-    }
-    elsif($result && ref($result) && $result->{'format'} && $result->{'content'}
-        && $result->{'format'} eq 'xls') {
-       print $cgi->header('-expires' => 'now',
-                   '-Content-Type' => 'application/vnd.ms-excel',
-                   '-Content-Disposition' => "attachment;filename=output.xls",
-                   '-Content-Length' => length($result->{'content'}),
-                   ),
-                   $result->{'content'};
-    }
-    elsif($result && ref($result) && $result->{'format'} && $result->{'content'}
-        && $result->{'format'} eq 'png') {
-       print $cgi->header('-expires' => 'now',
-                   '-Content-Type' => 'image/png',
-                   ),
-                   $result->{'content'};
-    }
-    else {
-       my $source = "$template_dir/$name.html";
-        my $template = new Text::Template( TYPE       => 'FILE',
-                                        SOURCE     => $source,
-                                        DELIMITERS => [ '<%=', '%>' ],
-                                        UNTAINT    => 1,
-                                      )
-       or die $Text::Template::ERROR;
-
-       my $data = $template->fill_in( 
-           PACKAGE => 'FS::SelfService::_selfservicecgi',
-           HASH    => $fill_in,
-         ) || "Error processing template $source"; # at least print _something_
-         print $cgi->header( '-expires' => 'now' );
-         print $data;
+  # update the user's authentication
+  my $timeout = $access_info->{'timeout'} || '3600';
+  my $cookie = CGI::Cookie->new('-name'     => 'session',
+                                '-value'    => $session_id,
+                                '-expires'  => '+'.$timeout.'s',
+                                #'-secure'   => 1, # would be a good idea...
+                               );
+  if ( $name eq 'logout' ) {
+    $cookie->expires(0);
+  }
+
+  if ( $fill_in->{'format'} ) {
+    # then override content-type, and return $fill_in->{'content'} instead
+    # of filling in a template
+    if ( $fill_in->{'format'} eq 'csv') {
+      print $cgi->header('-expires' => 'now',
+        '-Content-Type' => 'text/csv',
+        '-Content-Disposition' => "attachment;filename=output.csv",
+      );
+    } elsif ( $fill_in->{'format'} eq 'xls' ) {
+      print $cgi->header('-expires' => 'now',
+        '-Content-Type' => 'application/vnd.ms-excel',
+        '-Content-Disposition' => "attachment;filename=output.xls",
+        '-Content-Length' => length($fill_in->{'content'}),
+      );
+    } elsif ( $fill_in->{'format'} eq 'png' ) {
+      print $cgi->header('-expires' => 'now',
+        '-Content-Type' => 'image/png',
+      );
     }
- }
+    print $fill_in->{'content'};
+  } else { # the usual case
+    my $source = "$template_dir/$name.html";
+    my $template = new Text::Template(
+      TYPE       => 'FILE',
+      SOURCE     => $source,
+      DELIMITERS => [ '<%=', '%>' ],
+      UNTAINT    => 1,
+    )
+      or die $Text::Template::ERROR;
+
+    my $data = $template->fill_in( 
+      PACKAGE => 'FS::SelfService::_selfservicecgi',
+      HASH    => $fill_in,
+    ) || "Error processing template $source"; # at least print _something_
+    print $cgi->header( '-cookie' => $cookie,
+                        '-expires' => 'now' );
+    print $data;
+  }
+}
 
 #*FS::SelfService::_selfservicecgi::include = \&Text::Template::fill_in_file;
 
@@ -985,7 +1214,7 @@ package FS::SelfService::_selfservicecgi;
 
 use HTML::Entities;
 use FS::SelfService qw(
-    regionselector popselector domainselector location_form didselector
+    regionselector popselector domainselector location_form didselector mason_comp
 );
 
 #false laziness w/agent.cgi
@@ -1008,4 +1237,4 @@ sub include {
 
 }
 
-1;
+