Merge branch 'FREESIDE_3_BRANCH' of git.freeside.biz:/home/git/freeside into 3.x
authorMark Wells <mark@freeside.biz>
Wed, 21 Dec 2016 23:37:36 +0000 (15:37 -0800)
committerMark Wells <mark@freeside.biz>
Wed, 21 Dec 2016 23:37:36 +0000 (15:37 -0800)
14 files changed:
FS-Test/bin/freeside-test-stop
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Setup.pm
FS/FS/Upgrade.pm
FS/FS/cust_main.pm
FS/FS/part_export/broadband_sqlradius.pm
FS/FS/part_export/sqlradius.pm
FS/t/suite/15-activate_encryption.t [new file with mode: 0755]
httemplate/view/directions.html
ng_selfservice/elements/add_password_validation.php [new file with mode: 0644]
ng_selfservice/images/error.png [new file with mode: 0644]
ng_selfservice/images/tick.png [new file with mode: 0644]
ng_selfservice/password.php
ng_selfservice/xmlrpc_validate_passwd.php [new file with mode: 0644]

index 5e221a8..ad355c3 100755 (executable)
@@ -22,7 +22,7 @@ if (sudo grep -q '^test:' /usr/local/etc/freeside/htpasswd); then
   oldhtpasswd=$( cd /usr/local/etc/freeside; \
                  ls |grep -P 'htpasswd_\d{8}' | \
                  sort -nr |head -1 )
-  if [ -f $oldhtpasswd ]; then
+  if [ -f /usr/local/etc/freeside/$oldhtpasswd ]; then
     echo "Renaming $oldhtpasswd to htpasswd."
     sudo mv /usr/local/etc/freeside/$oldhtpasswd \
       /usr/local/etc/freeside/htpasswd
index de3e55d..0420d1a 100644 (file)
@@ -176,6 +176,7 @@ sub ss2clientapi {
   'reset_passwd'              => 'MyAccount/reset_passwd',
   'check_reset_passwd'        => 'MyAccount/check_reset_passwd',
   'process_reset_passwd'      => 'MyAccount/process_reset_passwd',
+  'validate_passwd'           => 'MyAccount/validate_passwd',
   'list_tickets'              => 'MyAccount/list_tickets',
   'create_ticket'             => 'MyAccount/create_ticket',
   'get_ticket'                => 'MyAccount/get_ticket',
index cac26c1..f092919 100644 (file)
@@ -84,6 +84,12 @@ sub enable_encryption {
   $conf->set('encryptionpublickey',  $rsa->get_public_key_string );
   $conf->set('encryptionprivatekey', $rsa->get_private_key_string );
 
+  # reload Record globals, false laziness with FS::Record
+  $FS::Record::conf_encryption           = $conf->exists('encryption');
+  $FS::Record::conf_encryptionmodule     = $conf->config('encryptionmodule');
+  $FS::Record::conf_encryptionpublickey  = join("\n",$conf->config('encryptionpublickey'));
+  $FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
 }
 
 sub populate_numbering {
index d06b7d8..576cdf0 100644 (file)
@@ -332,7 +332,8 @@ sub upgrade_data {
     #fix whitespace - before cust_main
     'cust_location' => [],
 
-    #cust_main (remove paycvv from history)
+    #cust_main (tokenizes cards, remove paycvv from history, locations, cust_payby, etc)
+    # (handles payinfo encryption/tokenization across all relevant tables)
     'cust_main' => [],
 
     #msgcat
index 0165bc4..2343fc6 100644 (file)
@@ -5734,6 +5734,90 @@ sub _upgrade_data { #class method
 
   $class->_upgrade_otaker(%opts);
 
+  # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
+  # existing records will be encrypted in queueable_upgrade (below)
+  unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
+    eval "use FS::Setup";
+    die $@ if $@;
+    FS::Setup::enable_encryption();
+  }
+
+}
+
+sub queueable_upgrade {
+  my $class = shift;
+
+  ### encryption gets turned on in _upgrade_data, above
+
+  eval "use FS::upgrade_journal";
+  die $@ if $@;
+
+  # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
+  # clear that out before encrypting/tokenizing anything else
+  if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
+    foreach my $table ('cust_main','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+      my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
+      my $sth = dbh->prepare($sql) or die dbh->errstr;
+      $sth->execute or die $sth->errstr;
+    }
+    FS::upgrade_journal->set_done('clear_payinfo_history');
+  }
+
+  # encrypt old records
+  if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) {
+
+    # allow replacement of closed cust_pay/cust_refund records
+    local $FS::payinfo_Mixin::allow_closed_replace = 1;
+
+    # because it looks like nothing's changing
+    local $FS::Record::no_update_diff = 1;
+
+    # commit everything immediately
+    local $FS::UID::AutoCommit = 1;
+
+    # encrypt what's there
+    foreach my $table ('cust_main','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+      my $tclass = 'FS::'.$table;
+      my $lastrecnum = 0;
+      my @recnums = ();
+      while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
+        my $record = $tclass->by_key($recnum);
+        next unless $record; # small chance it's been deleted, that's ok
+        next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+        # window for possible conflict is practically nonexistant,
+        #   but just in case...
+        $record = $record->select_for_update;
+        my $error = $record->replace;
+        die $error if $error;
+      }
+    }
+
+    FS::upgrade_journal->set_done('encryption_check');
+  }
+
+}
+
+# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
+# cust_payby might get deleted while this runs
+# not a method!
+sub _upgrade_next_recnum {
+  my ($dbh,$table,$lastrecnum,$recnums) = @_;
+  my $recnum = shift @$recnums;
+  return $recnum if $recnum;
+  my $tclass = 'FS::'.$table;
+  my $sql = 'SELECT '.$tclass->primary_key.
+            ' FROM '.$table.
+            ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
+            ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+  my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+  $sth->execute() or die $sth->errstr;
+  my @recnums;
+  while (my $rec = $sth->fetchrow_hashref) {
+    push @$recnums, $rec->{$tclass->primary_key};
+  }
+  $sth->finish();
+  $$lastrecnum = $$recnums[-1];
+  return shift @$recnums;
 }
 
 =back
index e58c641..2d6681e 100644 (file)
@@ -133,6 +133,8 @@ sub radius_check_suspended {
 sub _export_suspend {
   my( $self, $svc_broadband ) = (shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
index 67f0c5c..99cd615 100644 (file)
@@ -26,6 +26,10 @@ tie %options, 'Tie::IxHash',
                    type    => 'select',
                    options => [qw( usergroup radusergroup ) ],
                  },
+  'skip_provisioning' => {
+    type  => 'checkbox',
+    label => 'Skip provisioning records to this database'
+  },
   'ignore_accounting' => {
     type  => 'checkbox',
     label => 'Ignore accounting records from this database'
@@ -154,6 +158,8 @@ sub radius_check { #override for other svcdb
 sub _export_insert {
   my($self, $svc_x) = (shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   foreach my $table (qw(reply check)) {
     my $method = "radius_$table";
     my %attrib = $self->$method($svc_x);
@@ -179,6 +185,8 @@ sub _export_insert {
 sub _export_replace {
   my( $self, $new, $old ) = (shift, shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -289,6 +297,8 @@ sub _export_replace {
 sub _export_suspend {
   my( $self, $svc_acct ) = (shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   my $new = $svc_acct->clone_suspended;
   
   local $SIG{HUP} = 'IGNORE';
@@ -360,6 +370,8 @@ sub _export_suspend {
 sub _export_unsuspend {
   my( $self, $svc_x ) = (shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -399,6 +411,8 @@ sub _export_unsuspend {
 sub _export_delete {
   my( $self, $svc_x ) = (shift, shift);
 
+  return '' if $self->option('skip_provisioning');
+
   my $jobnum = '';
 
   my $usergroup = $self->option('usergroup') || 'usergroup';
diff --git a/FS/t/suite/15-activate_encryption.t b/FS/t/suite/15-activate_encryption.t
new file mode 100755 (executable)
index 0000000..e81ddf4
--- /dev/null
@@ -0,0 +1,110 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::Test;
+use Test::More tests => 14;
+use FS::Conf;
+use FS::UID qw( dbh );
+use DateTime;
+use FS::cust_main; # to load all other tables
+
+my $fs = FS::Test->new( user => 'admin' );
+my $conf = FS::Conf->new;
+my $err;
+my @tables = qw(cust_main cust_pay_pending cust_pay cust_pay_void cust_refund);
+
+### can only run on test database (company name "Freeside Test")
+like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
+
+### upgrade test db schema
+$err = system('freeside-upgrade','-s','admin');
+ok( !$err, 'schema upgrade ran' ) or BAIL_OUT('Error string: '.$!);
+
+### we need to unencrypt our test db before we can test turning it on
+
+# temporarily load all payinfo into memory
+my %payinfo = ();
+foreach my $table (@tables) {
+  $payinfo{$table} = {};
+  foreach my $record ($fs->qsearch({ table => $table })) {
+    next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+    $payinfo{$table}{$record->get($record->primary_key)} = $record->get('payinfo');
+  }
+}
+
+# turn off encryption
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+  $conf->delete($config);
+  ok( !$conf->exists($config), "deleted $config" ) or BAIL_OUT('');
+}
+$FS::Record::conf_encryption           = $conf->exists('encryption');
+$FS::Record::conf_encryptionmodule     = $conf->config('encryptionmodule');
+$FS::Record::conf_encryptionpublickey  = join("\n",$conf->config('encryptionpublickey'));
+$FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
+# save unencrypted values
+foreach my $table (@tables) {
+  local $FS::payinfo_Mixin::allow_closed_replace = 1;
+  local $FS::Record::no_update_diff = 1;
+  local $FS::UID::AutoCommit = 1;
+  my $tclass = 'FS::'.$table;
+  foreach my $key (keys %{$payinfo{$table}}) {
+    my $record = $tclass->by_key($key);
+    $record->payinfo($payinfo{$table}{$key});
+    $err = $record->replace;
+    last if $err;
+  }
+}
+ok( !$err, "save unencrypted values" ) or BAIL_OUT($err);
+
+# make sure it worked
+CHECKDECRYPT:
+foreach my $table (@tables) {
+  my $tclass = 'FS::'.$table;
+  foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+    my $sql = 'SELECT * FROM '.$table.
+              ' WHERE payinfo LIKE \'M%\''.
+              ' AND char_length(payinfo) > 80'.
+              ' AND '.$tclass->primary_key.' = '.$key;
+    my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+    $sth->execute or BAIL_OUT($sth->errstr);
+    if (my $hashrec = $sth->fetchrow_hashref) {
+      $err = $table.' '.$key.' encrypted';
+      last CHECKDECRYPT;
+    }
+  }
+}
+ok( !$err, "all values unencrypted" ) or BAIL_OUT($err);
+
+### now, run upgrade
+$err = system('freeside-upgrade','admin');
+ok( !$err, 'upgrade ran' ) or BAIL_OUT('Error string: '.$!);
+
+# check that confs got set
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+  ok( $conf->exists($config), "$config was set" ) or BAIL_OUT('');
+}
+
+# check that known records got encrypted
+CHECKENCRYPT:
+foreach my $table (@tables) {
+  my $tclass = 'FS::'.$table;
+  foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+    my $sql = 'SELECT * FROM '.$table.
+              ' WHERE payinfo LIKE \'M%\''.
+              ' AND char_length(payinfo) > 80'.
+              ' AND '.$tclass->primary_key.' = '.$key;
+    my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+    $sth->execute or BAIL_OUT($sth->errstr);
+    unless ($sth->fetchrow_hashref) {
+      $err = $table.' '.$key.' not encrypted';
+      last CHECKENCRYPT;
+    }
+  }
+}
+ok( !$err, "all values encrypted" ) or BAIL_OUT($err);
+
+exit;
+
+1;
+
index 8377d12..1c99cda 100644 (file)
@@ -62,7 +62,8 @@ function show_route() {
     if ( status == google.maps.DirectionsStatus.OK ) {
       directionsDisplay.setDirections(result);
     } else { 
-      document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>');
+      document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>')
+        + <% include('/elements/google_maps_api_key.html' ) |js_string%>;
     }
   });
 }
diff --git a/ng_selfservice/elements/add_password_validation.php b/ng_selfservice/elements/add_password_validation.php
new file mode 100644 (file)
index 0000000..6938437
--- /dev/null
@@ -0,0 +1,51 @@
+<SCRIPT>
+function add_password_validation (fieldid,nologin) {
+  var inputfield = document.getElementById(fieldid);
+  inputfield.onchange = function () {
+    var fieldid = this.id+'_result';
+    var resultfield = document.getElementById(fieldid);
+    var svcnum = '';
+    var svcfield = document.getElementById(this.id+'_svcnum');
+    if (svcfield) {
+      svcnum = svcfield.options[svcfield.selectedIndex].value;
+    }
+    if (this.value) {
+      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      var validate_data = {
+        fieldid: fieldid,
+        check_password: this.value,
+      };
+      if (!nologin) {
+        validate_data['svcnum'] = svcnum;
+      }
+      $.ajax({
+        url: 'xmlrpc_validate_passwd.php',
+        data: validate_data,
+        method: 'POST',
+        success: function ( result ) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(fieldid);
+          if (resultfield) {
+            var errorimg = '<IMG SRC="images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            var validimg = '<IMG SRC="images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            if (result.password_valid) {
+              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
+            } else if (result.password_invalid) {
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.password_invalid+'</SPAN>';
+            } else {
+              resultfield.innerHTML = '';
+            }
+          }
+        },
+        error: function (  jqXHR, textStatus, errorThrown ) {
+          var resultfield = document.getElementById(fieldid);
+          console.log('ajax error: '+textStatus+'+'+errorThrown);
+          if (resultfield) {
+            resultfield.innerHTML = '';
+          }
+        },
+      });
+    }
+  };
+}
+</SCRIPT>
diff --git a/ng_selfservice/images/error.png b/ng_selfservice/images/error.png
new file mode 100644 (file)
index 0000000..628cf2d
Binary files /dev/null and b/ng_selfservice/images/error.png differ
diff --git a/ng_selfservice/images/tick.png b/ng_selfservice/images/tick.png
new file mode 100644 (file)
index 0000000..a9925a0
Binary files /dev/null and b/ng_selfservice/images/tick.png differ
index 41296ed..a6e6795 100644 (file)
@@ -1,5 +1,92 @@
 <? $title ='Change Password'; include('elements/header.php'); ?>
 <? $current_menu = 'password.php'; include('elements/menu.php'); ?>
-Chagne password
+<?
+$error = '';
+$pwd_change_success = false;
+if ( isset($_POST['svcnum']) ) {
+
+  $pwd_change_result = $freeside->myaccount_passwd(array(
+    'session_id'    => $_COOKIE['session_id'],
+    'svcnum'        => $_POST['svcnum'],
+    'new_password'  => $_POST['new_password'],
+    'new_password2' => $_POST['new_password2'],
+  ));
+
+  if ($pwd_change_result['error']) {
+    $error = $pwd_change_result['error'];
+  } else {
+    $pwd_change_success = true;
+  }
+}
+
+if ($pwd_change_success) {
+?>
+
+<P>Password changed for <? echo $pwd_change_result['value'],' ',$pwd_change_result['label'] ?>.</P>
+
+<?
+} else {
+  $pwd_change_svcs = $freeside->list_svcs(array(
+    'session_id' => $_COOKIE['session_id'],
+    'svcdb'      => 'svc_acct',
+  ));
+  if (isset($pwd_change_svcs['error'])) {
+    $error = $error || $pwd_change_svcs['error'];
+  }
+  if (!isset($pwd_change_svcs['svcs'])) {
+    $pwd_change_svcs['svcs'] = $pwd_change_svcs['svcs'];
+    $error = $error || 'Unknown error loading services';
+  }
+  if ($error) {
+    include('elements/error.php');
+  }
+?>
+
+<FORM METHOD="POST">
+<TABLE BGCOLOR="#cccccc">
+  <TR>
+    <TH ALIGN="right">Change password for account: </TH>
+    <TD>
+      <SELECT ID="new_password_svcnum" NAME="svcnum">
+<?
+  $selected_svcnum = isset($_POST['svcnum']) ? $_POST['svcnum'] : $pwd_change_svcs['svcnum'];
+  foreach ($pwd_change_svcs['svcs'] as $svc) {
+?>
+        <OPTION VALUE="<? echo $svc['svcnum'] ?>"<? echo $selected_svcnum == $svc['svcnum'] ? ' SELECTED' : '' ?>>
+          <? echo $svc['label'],': ',$svc['value'] ?>
+        </OPTION>
+<?
+  }
+?>
+      </SELECT>
+    </TD>
+  </TR>
+
+  <TR>
+    <TH ALIGN="right">New password: </TH>
+    <TD>
+      <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+      <DIV ID="new_password_result"></DIV>
+<? include('elements/add_password_validation.php'); ?>
+      <SCRIPT>add_password_validation('new_password');</SCRIPT>
+    </TD>
+  </TR>
+
+  <TR>
+    <TH ALIGN="right">Re-enter new password: </TH>
+    <TD><INPUT TYPE="password" NAME="new_password2" SIZE="18"></TD>
+  </TR>
+
+</TABLE>
+<BR>
+
+<INPUT TYPE="submit" VALUE="Change password">
+
+</FORM>
+
+<?
+} // end if $pwd_change_show_form
+?>
+
 <? include('elements/menu_footer.php'); ?>
 <? include('elements/footer.php'); ?>
diff --git a/ng_selfservice/xmlrpc_validate_passwd.php b/ng_selfservice/xmlrpc_validate_passwd.php
new file mode 100644 (file)
index 0000000..5632dc3
--- /dev/null
@@ -0,0 +1,15 @@
+<?
+
+require_once('elements/session.php');
+
+$xmlrpc_args = array(
+  fieldid        => $_POST['fieldid'],
+  check_password => $_POST['check_password'],
+  svcnum         => $_POST['svcnum'],
+  session_id     => $_COOKIE['session_id']
+);
+
+$result = $freeside->validate_passwd($xmlrpc_args);
+echo json_encode($result);
+
+?>