[freeside-commits] branch FREESIDE_4_BRANCH updated. 5a813d91f3d4b1c6eff81dfa9c88e2b587442984

Jonathan Prykop jonathan at 420.am
Mon Dec 14 20:33:05 PST 2015


The branch, FREESIDE_4_BRANCH has been updated
       via  5a813d91f3d4b1c6eff81dfa9c88e2b587442984 (commit)
       via  8cd6e05d5d906da6b001b36bab5aa87ecdfca944 (commit)
       via  4ff9a50fe7f35179314967d71ae66b696ab006c5 (commit)
       via  32b783795ee3a39752fc72f2c861eac8cdb6d12a (commit)
      from  a2d1bca6d13c6760f2c7c2de677da4df3f9e5c3e (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 5a813d91f3d4b1c6eff81dfa9c88e2b587442984
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Mon Dec 7 17:46:45 2015 -0600

    RT#29354: Password Security in Email [customer fields, images, js files]

diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm
index 3129366..834fd6f 100644
--- a/FS/FS/Password_Mixin.pm
+++ b/FS/FS/Password_Mixin.pm
@@ -67,6 +67,40 @@ sub is_password_allowed {
 
   return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert
 
+  #check against customer fields
+  my $cust_main = $self->cust_main;
+  if ($cust_main) {
+    my @words;
+    # words from cust_main
+    foreach my $field ( qw( last first daytime night fax mobile ) ) {
+        push @words, split(/\W/,$cust_main->get($field));
+    }
+    # words from cust_location
+    foreach my $loc ($cust_main->cust_location) {
+      foreach my $field ( qw(address1 address2 city county state zip) ) {
+        push @words, split(/\W/,$loc->get($field));
+      }
+    }
+    # words from cust_contact & contact_phone
+    foreach my $contact (map { $_->contact } $cust_main->cust_contact) {
+      foreach my $field ( qw(last first) ) {
+        push @words, split(/\W/,$contact->get($field));
+      }
+      # not hugely useful right now, hyphenless stored values longer than password max,
+      # but max will probably be increased eventually...
+      foreach my $phone ( qsearch('contact_phone', {'contactnum' => $contact->contactnum}) ) {
+        push @words, split(/\W/,$phone->get('phonenum'));
+      }
+    }
+    # do the actual checking
+    foreach my $word (@words) {
+      next unless length($word) > 2;
+      if ($password =~ /$word/i) {
+        return qq(Password contains account information '$word');
+      }
+    }
+  }
+
   my $no_reuse = 3;
   # allow override here if we really must
 
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index 38cebc1..53b12f1 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -2686,6 +2686,7 @@ sub password_svc_check {
   my ($self, $password) = @_;
   foreach my $field ( qw(username finger) ) {
     foreach my $word (split(/\W+/,$self->get($field))) {
+      next unless length($word) > 2;
       if ($password =~ /$word/i) {
         return qq(Password contains account information '$word');
       }
diff --git a/fs_selfservice/FS-SelfService/cgi/add_password_validation.html b/fs_selfservice/FS-SelfService/cgi/add_password_validation.js
similarity index 54%
rename from fs_selfservice/FS-SelfService/cgi/add_password_validation.html
rename to fs_selfservice/FS-SelfService/cgi/add_password_validation.js
index e349fd7..e2e3227 100644
--- a/fs_selfservice/FS-SelfService/cgi/add_password_validation.html
+++ b/fs_selfservice/FS-SelfService/cgi/add_password_validation.js
@@ -1,5 +1,4 @@
-<SCRIPT>
-function add_password_validation (fieldid) {
+function add_password_validation (fieldid,nologin) {
   var inputfield = document.getElementById(fieldid);
   inputfield.onchange = function () {
     var fieldid = this.id+'_result';
@@ -11,19 +10,22 @@ function add_password_validation (fieldid) {
     }
     if (this.value) {
       resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      var action = nologin ? 'validate_password_nologin' : 'validate_password';
       send_xmlhttp('selfservice.cgi',
-        ['action','validate_password','fieldid',fieldid,'svcnum',svcnum,'check_password',this.value],
+        ['action',action,'fieldid',fieldid,'svcnum',svcnum,'check_password',this.value],
         function (result) {
           result = JSON.parse(result);
           var resultfield = document.getElementById(result.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.valid) {
-              resultfield.innerHTML = '<SPAN STYLE="color: green;">Password valid!</SPAN>';
+              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
             } else if (result.error) {
-              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
             } else {
               result.syserror = result.syserror || 'Server error';
-              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
             }
           }
         }
@@ -33,4 +35,4 @@ function add_password_validation (fieldid) {
     }
   };
 }
-</SCRIPT>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/change_password.html b/fs_selfservice/FS-SelfService/cgi/change_password.html
index ef66554..879faf2 100644
--- a/fs_selfservice/FS-SelfService/cgi/change_password.html
+++ b/fs_selfservice/FS-SelfService/cgi/change_password.html
@@ -28,11 +28,11 @@
     <TD>
       <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
       <DIV ID="new_password_result"></DIV>
-<%= include('send_xmlhttp') %>
-<%= include('add_password_validation') %>
-<SCRIPT>
-add_password_validation('new_password');
-</SCRIPT>
+      <SCRIPT SRC="send_xmlhttp.js"></SCRIPT>
+      <SCRIPT SRC="add_password_validation.js"></SCRIPT>
+      <SCRIPT>
+      add_password_validation('new_password');
+      </SCRIPT>
     </TD>
   </TR>
 
diff --git a/fs_selfservice/FS-SelfService/cgi/images/error.png b/fs_selfservice/FS-SelfService/cgi/images/error.png
new file mode 100644
index 0000000..628cf2d
Binary files /dev/null and b/fs_selfservice/FS-SelfService/cgi/images/error.png differ
diff --git a/fs_selfservice/FS-SelfService/cgi/images/tick.png b/fs_selfservice/FS-SelfService/cgi/images/tick.png
new file mode 100644
index 0000000..a9925a0
Binary files /dev/null and b/fs_selfservice/FS-SelfService/cgi/images/tick.png differ
diff --git a/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html b/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html
index ec672c8..da6fc8d 100644
--- a/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html
+++ b/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html
@@ -15,6 +15,7 @@
 <INPUT TYPE="hidden" NAME="session_id" VALUE="<%= $session_id %>">
 <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
 
+<DIV STYLE="background: <%= $box_bgcolor || '#c0c0c0' %>">
 <TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
 
 <%= if (!$error) {
@@ -23,16 +24,27 @@
 
   <TR>
     <TH ALIGN="right">New password: </TH>
-    <TD><INPUT TYPE="password" NAME="new_password" SIZE="18"></TD>
+    <TD>
+      <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+    </TD>
+    <TD>
+      <SPAN ID="new_password_result"></SPAN>
+      <SCRIPT SRC="send_xmlhttp.js"></SCRIPT>
+      <SCRIPT SRC="add_password_validation.js"></SCRIPT>
+      <SCRIPT>
+      add_password_validation('new_password',true);
+      </SCRIPT>
+    </TD>
   </TR>
 
   <TR>
     <TH ALIGN="right">Re-enter new password: </TH>
     <TD><INPUT TYPE="password" NAME="new_password2" SIZE="18"></TD>
+    <TD></TD>
   </TR>
-
   <TR>
-    <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" VALUE="Change password"></TD>
+    <TD COLSPAN="2" ALIGN="center"><INPUT TYPE="submit" VALUE="Change password"></TD>
+    <TD></TD>
   </TR>
 END
 
@@ -40,6 +52,7 @@ END
 %>
 
 </TABLE>
+</DIV>
 </FORM>
 
 <%= $body_footer %>
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index 5845122..aff9bca 100755
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -95,6 +95,7 @@ my @nologin_actions = (qw(
   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;
@@ -1132,6 +1133,14 @@ sub validate_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 {
diff --git a/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html b/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.js
similarity index 98%
rename from fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html
rename to fs_selfservice/FS-SelfService/cgi/send_xmlhttp.js
index ac85cb2..e299168 100644
--- a/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html
+++ b/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.js
@@ -1,4 +1,3 @@
-<SCRIPT>
 function rs_init_object () {
   var A;
   try {
@@ -41,5 +40,4 @@ function send_xmlhttp (url,args,callback) {
   xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
   xmlhttp.send(content);
 }
-</SCRIPT>
 
diff --git a/fs_selfservice/FS-SelfService/cgi/signup.cgi b/fs_selfservice/FS-SelfService/cgi/signup.cgi
index 072ce96..817fdd3 100755
--- a/fs_selfservice/FS-SelfService/cgi/signup.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/signup.cgi
@@ -508,31 +508,3 @@ use FS::SelfService qw( regionselector expselect popselector domainselector
                         didselector
                       );
 
-sub add_password_validation {
-  my $fieldid = shift;
-  my $out = '';
-  if ((-e './send_xmlhttp.html') && (-e './add_password_validation.html')) {
-    my $template = new Text::Template( TYPE   => 'FILE',
-                                       SOURCE => "./send_xmlhttp.html",
-                                       DELIMITERS => [ '<%=', '%>' ],
-                                       UNTAINT => 1,                   
-                                     )
-      or die $Text::Template::ERROR;
-    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
-    $template = new Text::Template( TYPE   => 'FILE',
-                                       SOURCE => "./add_password_validation.html",
-                                       DELIMITERS => [ '<%=', '%>' ],
-                                       UNTAINT => 1,                   
-                                     )
-      or die $Text::Template::ERROR;
-    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
-    $out .= <<ENDOUT;
-<SCRIPT>
-add_password_validation('$fieldid');
-</SCRIPT>
-ENDOUT
-  }
-  return $out;
-}
-
-
diff --git a/fs_selfservice/FS-SelfService/cgi/signup.html b/fs_selfservice/FS-SelfService/cgi/signup.html
index 5900ba6..def5299 100755
--- a/fs_selfservice/FS-SelfService/cgi/signup.html
+++ b/fs_selfservice/FS-SelfService/cgi/signup.html
@@ -386,12 +386,12 @@ ENDOUT
   <TD ALIGN="right">Password</TD>
   <TD>
     <INPUT ID="new_password" TYPE="password" NAME="_password" VALUE="$_password">
-    <DIV ID="new_password_result"></DIV>
-ENDOUT
-
-   $OUT .= add_password_validation('new_password');
-
-   $OUT .= <<ENDOUT;
+    <SPAN ID="new_password_result"></SPAN>
+    <SCRIPT SRC="send_xmlhttp.js"></SCRIPT>
+    <SCRIPT SRC="add_password_validation.js"></SCRIPT>
+    <SCRIPT>
+    add_password_validation('new_password',true);
+    </SCRIPT>
   </TD>
 </TR>
 <TR>
diff --git a/httemplate/elements/validate_password.html b/httemplate/elements/validate_password.html
index fd2cb6c..a488c4f 100644
--- a/httemplate/elements/validate_password.html
+++ b/httemplate/elements/validate_password.html
@@ -32,13 +32,15 @@ function add_password_validation (fieldid) {
           result = JSON.parse(result);
           var resultfield = document.getElementById(result.fieldid);
           if (resultfield) {
+            var errorimg = '<IMG SRC="<% $p %>images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            var validimg = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
             if (result.valid) {
-              resultfield.innerHTML = '<SPAN STYLE="color: green;">Password valid!</SPAN>';
+              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
             } else if (result.error) {
-              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
             } else {
               result.syserror = result.syserror || 'Server error';
-              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
             }
           }
         }

commit 8cd6e05d5d906da6b001b36bab5aa87ecdfca944
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Wed Dec 2 05:02:17 2015 -0600

    RT#29354: Password Security in Email [xmlhttp validation for selfservice]

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 9847e5f..33a8e61 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -3308,6 +3308,45 @@ sub process_reset_passwd {
 
 }
 
+sub validate_passwd {
+  my $p = shift;
+
+  my %result;
+  %result = ( 'fieldid' => $p->{'fieldid'} )
+    if $p->{'fieldid'} =~ /^\w+$/;
+
+  return { %result, 'password_invalid' => 'Enter new password' }
+    unless length($p->{'check_password'});
+
+  my $svc_acct;
+  if ($p->{'svcnum'}) {
+    # false laziness with myaccount_passwd
+    my($context, $session, $custnum) = _custoragent_session_custnum($p);
+    return { %result, 'error' => $session } if $context eq 'error';
+
+    $custnum =~ /^(\d+)$/ or die "illegal custnum";
+    my $search = " AND custnum = $1";
+    $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+
+    $svc_acct = qsearchs( {
+      'table'     => 'svc_acct',
+      'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
+                     'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
+                     'LEFT JOIN cust_main USING ( custnum ) ',
+      'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
+      'extra_sql' => $search, #important
+    } )
+      or return { %result, 'error' => "Service not found" };
+    # end false laziness
+  }
+
+  $svc_acct ||= new FS::svc_acct {};
+
+  my $error = $svc_acct->is_password_allowed($p->{'check_password'});
+  return { %result, 'password_invalid' => $error } if $error;
+  return { %result, 'password_valid' => 1 };
+}
+
 sub list_tickets {
   my $p = shift;
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm
index f4b47b2..4023aa8 100644
--- a/fs_selfservice/FS-SelfService/SelfService.pm
+++ b/fs_selfservice/FS-SelfService/SelfService.pm
@@ -99,6 +99,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   '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',
diff --git a/fs_selfservice/FS-SelfService/cgi/add_password_validation.html b/fs_selfservice/FS-SelfService/cgi/add_password_validation.html
new file mode 100644
index 0000000..e349fd7
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/add_password_validation.html
@@ -0,0 +1,36 @@
+<SCRIPT>
+function add_password_validation (fieldid) {
+  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>';
+      send_xmlhttp('selfservice.cgi',
+        ['action','validate_password','fieldid',fieldid,'svcnum',svcnum,'check_password',this.value],
+        function (result) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(result.fieldid);
+          if (resultfield) {
+            if (result.valid) {
+              resultfield.innerHTML = '<SPAN STYLE="color: green;">Password valid!</SPAN>';
+            } else if (result.error) {
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+            } else {
+              result.syserror = result.syserror || 'Server error';
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+            }
+          }
+        }
+      );
+    } else {
+      resultfield.innerHTML = '';
+    }
+  };
+}
+</SCRIPT>
diff --git a/fs_selfservice/FS-SelfService/cgi/change_password.html b/fs_selfservice/FS-SelfService/cgi/change_password.html
index 22d8973..ef66554 100644
--- a/fs_selfservice/FS-SelfService/cgi/change_password.html
+++ b/fs_selfservice/FS-SelfService/cgi/change_password.html
@@ -12,7 +12,7 @@
   <TR>
     <TH ALIGN="right">Change password for account: </TH>
     <TD>
-      <SELECT NAME="svcnum">
+      <SELECT ID="new_password_svcnum" NAME="svcnum">
         <%= foreach my $svc ( @svcs ) {
               $OUT .= '<OPTION VALUE="'. $svc->{'svcnum'}. '"'.
                         ( $svc->{'svcnum'} eq $svcnum ? ' SELECTED' : '' ). '>'.
@@ -25,7 +25,15 @@
 
   <TR>
     <TH ALIGN="right">New password: </TH>
-    <TD><INPUT TYPE="password" NAME="new_password" SIZE="18"></TD>
+    <TD>
+      <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+      <DIV ID="new_password_result"></DIV>
+<%= include('send_xmlhttp') %>
+<%= include('add_password_validation') %>
+<SCRIPT>
+add_password_validation('new_password');
+</SCRIPT>
+    </TD>
   </TR>
 
   <TR>
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index b2ebaef..5845122 100755
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -23,6 +23,7 @@ use FS::SelfService qw(
   mason_comp port_graph
   start_thirdparty finish_thirdparty
   reset_passwd check_reset_passwd process_reset_passwd
+  validate_passwd
   billing_history
 );
 
@@ -85,6 +86,7 @@ my @actions = ( qw(
   process_suspend_pkg
   switch_cust
   history
+  validate_password
 ));
 
 my @nologin_actions = (qw(
@@ -109,7 +111,6 @@ if ( $cgi->param('action') =~ /^process_forgot_password_session_(\w+)$/ ) {
     warn "WARNING: unrecognized action '$1'\n";
   }
 }
-
 unless ( $nologin_actions{$action} ) {
 
   my %cookies = CGI::Cookie->fetch;
@@ -1123,6 +1124,14 @@ sub do_process_forgot_password {
   );
 }
 
+sub validate_password {
+  validate_passwd(
+    'session_id' => $session_id,
+    map { $_ => scalar($cgi->param($_)) }
+      qw( fieldid svcnum check_password )
+  )
+}
+
 #--
 
 sub do_template {
diff --git a/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html b/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html
new file mode 100644
index 0000000..ac85cb2
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html
@@ -0,0 +1,45 @@
+<SCRIPT>
+function rs_init_object () {
+  var A;
+  try {
+    A=new ActiveXObject("Msxml2.XMLHTTP");
+  } catch (e) {
+    try {
+      A=new ActiveXObject("Microsoft.XMLHTTP");
+    } catch (oc) {
+      A=null;
+    }
+  }
+  if(!A && typeof XMLHttpRequest != "undefined")
+    A = new XMLHttpRequest();
+  if (!A)
+    alert("Can't create XMLHttpRequest object");
+  return A;
+}
+
+function send_xmlhttp (url,args,callback) {
+  args = args || [];
+  callback = callback || function (data) { return data };
+  var content = '';
+  for (var i = 0; i < args.length; i = i + 2) {
+    content = content + "&" + args[i] + "=" + escape(args[i+1]);
+  }
+  content = content.replace( /[+]/g, '%2B'); // fix unescaped plus signs 
+
+  var xmlhttp = rs_init_object();
+  xmlhttp.open("POST", url, true);
+
+  xmlhttp.onreadystatechange = function() {
+    if (xmlhttp.readyState != 4) 
+      return;
+    if (xmlhttp.status == 200) {
+      var data = xmlhttp.responseText;
+      callback(data);
+    }
+  };
+
+  xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+  xmlhttp.send(content);
+}
+</SCRIPT>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/signup.cgi b/fs_selfservice/FS-SelfService/cgi/signup.cgi
index 817fdd3..072ce96 100755
--- a/fs_selfservice/FS-SelfService/cgi/signup.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/signup.cgi
@@ -508,3 +508,31 @@ use FS::SelfService qw( regionselector expselect popselector domainselector
                         didselector
                       );
 
+sub add_password_validation {
+  my $fieldid = shift;
+  my $out = '';
+  if ((-e './send_xmlhttp.html') && (-e './add_password_validation.html')) {
+    my $template = new Text::Template( TYPE   => 'FILE',
+                                       SOURCE => "./send_xmlhttp.html",
+                                       DELIMITERS => [ '<%=', '%>' ],
+                                       UNTAINT => 1,                   
+                                     )
+      or die $Text::Template::ERROR;
+    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
+    $template = new Text::Template( TYPE   => 'FILE',
+                                       SOURCE => "./add_password_validation.html",
+                                       DELIMITERS => [ '<%=', '%>' ],
+                                       UNTAINT => 1,                   
+                                     )
+      or die $Text::Template::ERROR;
+    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
+    $out .= <<ENDOUT;
+<SCRIPT>
+add_password_validation('$fieldid');
+</SCRIPT>
+ENDOUT
+  }
+  return $out;
+}
+
+
diff --git a/fs_selfservice/FS-SelfService/cgi/signup.html b/fs_selfservice/FS-SelfService/cgi/signup.html
index 2bc59ca..5900ba6 100755
--- a/fs_selfservice/FS-SelfService/cgi/signup.html
+++ b/fs_selfservice/FS-SelfService/cgi/signup.html
@@ -336,7 +336,8 @@ HTML::Widgets::SelectLayers->new(
 <FORM name="signup_form" action="<%= $self_url %>" METHOD="POST" onsubmit="return fixup_form();"><BR><FONT SIZE="+1"><B>First package</B></FONT>
 <INPUT TYPE="hidden" NAME="promo_code" VALUE="<%= $promo_code %>">
 <INPUT TYPE="hidden" NAME="reg_code" VALUE="<%= $reg_code %>">
-<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<DIV STYLE="background: <%= $box_bgcolor %>; width: 100%">
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0>
 <TR>
   <TD COLSPAN=2><SELECT NAME="pkgpart">
 
@@ -383,7 +384,15 @@ ENDOUT
     $OUT .= <<ENDOUT;
 <TR>
   <TD ALIGN="right">Password</TD>
-  <TD><INPUT TYPE="password" NAME="_password" VALUE="$_password"></TD>
+  <TD>
+    <INPUT ID="new_password" TYPE="password" NAME="_password" VALUE="$_password">
+    <DIV ID="new_password_result"></DIV>
+ENDOUT
+
+   $OUT .= add_password_validation('new_password');
+
+   $OUT .= <<ENDOUT;
+  </TD>
 </TR>
 <TR>
   <TD ALIGN="right">Re-enter Password</TD>
@@ -433,6 +442,7 @@ NOMADIX
 %>
 
 </TABLE>
+</DIV>
 
 <%= 
 if ( @optional_packages ) { 
diff --git a/fs_selfservice/FS-SelfService/cgi/validate_password.html b/fs_selfservice/FS-SelfService/cgi/validate_password.html
new file mode 100644
index 0000000..5cc3167
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/validate_password.html
@@ -0,0 +1,9 @@
+<%= use JSON; 
+    encode_json({
+      'valid'    => $password_valid ? 1 : 0,
+      'error'    => $password_invalid,
+      'syserror' => $error,
+      'fieldid'  => $fieldid,
+      'password_debug' => $password_debug,
+    }); %>
+
diff --git a/httemplate/elements/change_password.html b/httemplate/elements/change_password.html
index 7d8daae..2b40ec1 100644
--- a/httemplate/elements/change_password.html
+++ b/httemplate/elements/change_password.html
@@ -8,7 +8,7 @@
   display: none;
 }
 </STYLE>
-<A ID="<%$pre%>link" HREF="#" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
+<A ID="<%$pre%>link" HREF="javascript:void(0)" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
 <DIV ID="<%$pre%>form" CLASS="passwordbox">
   <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html">
     <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svc_acct->svcnum |h%>">

commit 4ff9a50fe7f35179314967d71ae66b696ab006c5
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Tue Dec 1 20:51:33 2015 -0600

    RT#29354: Password Security in Email [password_svc_check and aspell requirement]

diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm
index bcad546..3129366 100644
--- a/FS/FS/Password_Mixin.pm
+++ b/FS/FS/Password_Mixin.pm
@@ -61,14 +61,9 @@ sub is_password_allowed {
   $error = 'Invalid password - ' . $error if $error;
   return $error if $error;
 
-  #check against known usernames
-  my @disallowed_names = $self->password_disallowed_names;
-  foreach my $noname (@disallowed_names) {
-    if ($password =~ /$noname/i) {
-      #keeping message ambiguous to avoid leaking personal info
-      return 'Password contains a disallowed word';
-    }
-  }
+  #check against service fields
+  $error = $self->password_svc_check($password);
+  return $error if $error;
 
   return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert
 
@@ -107,15 +102,15 @@ sub is_password_allowed {
   '';
 }
 
-=item password_disallowed_names
+=item password_svc_check
 
-Override to return a list additional words (eg usernames) not
-to be used by passwords on this service.
+Override to run additional service-specific password checks.
 
 =cut
 
-sub password_disallowed_names {
-  return ();
+sub password_svc_check {
+  my ($self, $password) = @_;
+  return '';
 }
 
 =item password_history_key
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index e7ec4a2..38cebc1 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -2676,29 +2676,22 @@ sub virtual_maildir {
   $self->domain. '/maildirs/'. $self->username. '/';
 }
 
-=item password_disallowed_names
+=item password_svc_check
 
 Override, for L<FS::Password_Mixin>.  Not really intended for other use.
 
 =cut
 
-sub password_disallowed_names {
-  my $self = shift;
-  my $dbh = dbh;
-  my $results = {};
-  foreach my $field ( qw( username finger ) ) {
-    my $sql = 'SELECT DISTINCT '.$field.' FROM svc_acct';
-    my $sth = $dbh->prepare($sql)
-      or die "Error preparing $sql: ". $dbh->errstr;
-    $sth->execute()
-      or die "Error executing $sql: ". $sth->errstr;
-    foreach my $row (@{$sth->fetchall_arrayref}, $self->get($field)) {
-      foreach my $word (split(/\s+/,$$row[0])) {
-        $results->{lc($word)} = 1;
+sub password_svc_check {
+  my ($self, $password) = @_;
+  foreach my $field ( qw(username finger) ) {
+    foreach my $word (split(/\W+/,$self->get($field))) {
+      if ($password =~ /$word/i) {
+        return qq(Password contains account information '$word');
       }
     }
   }
-  return keys %$results;
+  return '';
 }
 
 =back
diff --git a/debian/control b/debian/control
index 5408f4c..b7b38c5 100644
--- a/debian/control
+++ b/debian/control
@@ -22,7 +22,7 @@ Description: Billing and trouble ticketing for service providers
 
 Package: freeside-lib
 Architecture: all
-Depends: gnupg,ghostscript,gsfonts,gzip,latex-xcolor,
+Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor,
  libbusiness-creditcard-perl,libcache-cache-perl,
  libcache-simple-timedexpiry-perl,libchart-perl,libclass-container-perl,
  libclass-data-inheritable-perl,libclass-returnvalue-perl,libcolor-scheme-perl,

commit 32b783795ee3a39752fc72f2c861eac8cdb6d12a
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Sat Nov 21 01:54:21 2015 -0600

    RT#29354: Password Security in Email

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index a6bde82..9847e5f 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -2995,12 +2995,6 @@ sub myaccount_passwd {
         )
     && ! $svc_acct->check_password($p->{'old_password'});
 
-    # should move password length checks into is_password_allowed
-  $error = 'Password too short.'
-    if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
-  $error = 'Password too long.'
-    if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
-
   $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
          ||  $svc_acct->set_password($p->{'new_password'})
          ||  $svc_acct->replace();
diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm
index 990f735..bcad546 100644
--- a/FS/FS/Password_Mixin.pm
+++ b/FS/FS/Password_Mixin.pm
@@ -6,8 +6,13 @@ use FS::password_history;
 use Authen::Passphrase;
 use Authen::Passphrase::BlowfishCrypt;
 # https://rt.cpan.org/Ticket/Display.html?id=72743
+use Data::Password qw(:all);
 
 our $DEBUG = 0;
+our $conf;
+FS::UID->install_callback( sub {
+  $conf = FS::Conf->new;
+});
 
 our $me = '[' . __PACKAGE__ . ']';
 
@@ -38,7 +43,34 @@ sub is_password_allowed {
   my $self = shift;
   my $password = shift;
 
-  # check length and complexity here
+  # basic checks using Data::Password;
+  # options for Data::Password
+  $DICTIONARY = 4;   # minimum length of disallowed words
+  $MINLEN = $conf->config('passwordmin') || 6;
+  $MAXLEN = $conf->config('passwordmax') || 8;
+  $GROUPS = 4;       # must have all 4 'character groups': numbers, symbols, uppercase, lowercase
+  # other options use the defaults listed below:
+  # $FOLLOWING = 3;    # disallows more than 3 chars in a row, by alphabet or keyboard (ie abcd or asdf)
+  # $SKIPCHAR = undef; # set to true to skip checking for bad characters
+  # # lists of disallowed words
+  # @DICTIONARIES = qw( /usr/share/dict/web2 /usr/share/dict/words /usr/share/dict/linux.words );
+
+  my $error = IsBadPassword($password);
+  $error = 'must contain at least one each of numbers, symbols, and lowercase and uppercase letters'
+    if $error eq 'contains less than 4 character groups'; # avoid confusion
+  $error = 'Invalid password - ' . $error if $error;
+  return $error if $error;
+
+  #check against known usernames
+  my @disallowed_names = $self->password_disallowed_names;
+  foreach my $noname (@disallowed_names) {
+    if ($password =~ /$noname/i) {
+      #keeping message ambiguous to avoid leaking personal info
+      return 'Password contains a disallowed word';
+    }
+  }
+
+  return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert
 
   my $no_reuse = 3;
   # allow override here if we really must
@@ -75,6 +107,17 @@ sub is_password_allowed {
   '';
 }
 
+=item password_disallowed_names
+
+Override to return a list additional words (eg usernames) not
+to be used by passwords on this service.
+
+=cut
+
+sub password_disallowed_names {
+  return ();
+}
+
 =item password_history_key
 
 Returns the name of the field in L<FS::password_history> that's the foreign
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index 9323976..e7ec4a2 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -2676,6 +2676,31 @@ sub virtual_maildir {
   $self->domain. '/maildirs/'. $self->username. '/';
 }
 
+=item password_disallowed_names
+
+Override, for L<FS::Password_Mixin>.  Not really intended for other use.
+
+=cut
+
+sub password_disallowed_names {
+  my $self = shift;
+  my $dbh = dbh;
+  my $results = {};
+  foreach my $field ( qw( username finger ) ) {
+    my $sql = 'SELECT DISTINCT '.$field.' FROM svc_acct';
+    my $sth = $dbh->prepare($sql)
+      or die "Error preparing $sql: ". $dbh->errstr;
+    $sth->execute()
+      or die "Error executing $sql: ". $sth->errstr;
+    foreach my $row (@{$sth->fetchall_arrayref}, $self->get($field)) {
+      foreach my $word (split(/\s+/,$$row[0])) {
+        $results->{lc($word)} = 1;
+      }
+    }
+  }
+  return keys %$results;
+}
+
 =back
 
 =head1 CLASS METHODS
diff --git a/debian/control b/debian/control
index 6534e2f..5408f4c 100644
--- a/debian/control
+++ b/debian/control
@@ -27,7 +27,8 @@ Depends: gnupg,ghostscript,gsfonts,gzip,latex-xcolor,
  libcache-simple-timedexpiry-perl,libchart-perl,libclass-container-perl,
  libclass-data-inheritable-perl,libclass-returnvalue-perl,libcolor-scheme-perl,
  libcompress-zlib-perl,libconvert-binhex-perl,libcrypt-passwdmd5-perl,
- libcrypt-ssleay-perl,libcss-squish-perl,libdate-manip-perl,libdbd-mysql-perl,
+ libcrypt-ssleay-perl,libcss-squish-perl,
+ libdata-password-perl,libdate-manip-perl,libdbd-mysql-perl,
  libdbd-pg-perl,libdbi-perl,libdbix-dbschema-perl,libdbix-searchbuilder-perl,
  libdevel-stacktrace-perl,libdevel-symdump-perl,liberror-perl,
  libexcel-writer-xlsx-perl,libexception-class-perl,libfile-counterfile-perl,
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
index 31678a9..0cf0c20 100755
--- a/httemplate/edit/svc_acct.cgi
+++ b/httemplate/edit/svc_acct.cgi
@@ -50,7 +50,12 @@
      'required' => $part_svc->part_svc_column('_password')->required ) %>
   <TD>
     <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
-    <& /elements/random_pass.html, 'clear_password' &>
+    <& /elements/random_pass.html, 'clear_password' &><BR>
+    <DIV ID="clear_password_result" STYLE="font-size: smaller"></DIV>
+    <& '/elements/validate_password.html', 
+         'fieldid' => 'clear_password',
+         'svcnum' => $svcnum 
+    &>
   </TD>
 </TR>
 %}else{
diff --git a/httemplate/elements/change_password.html b/httemplate/elements/change_password.html
index 625ba1f..7d8daae 100644
--- a/httemplate/elements/change_password.html
+++ b/httemplate/elements/change_password.html
@@ -16,6 +16,12 @@
     <& /elements/random_pass.html, $pre.'password', 'randomize' &>
     <INPUT TYPE="submit" VALUE="change">
     <INPUT TYPE="button" VALUE="cancel" onclick="<%$pre%>toggle(false)">
+    <DIV ID="<%$pre%>password_result" STYLE="font-size: smaller"></DIV>
+    <& '/elements/validate_password.html', 
+         'fieldid' => $pre.'password',
+         'svcnum'  => $svc_acct->svcnum,
+
+    &>
 % if ( $error ) {
     <BR><SPAN STYLE="color: #ff0000"><% $error |h %></SPAN>
 % }
diff --git a/httemplate/elements/random_pass.html b/httemplate/elements/random_pass.html
index b215b77..14bbb58 100644
--- a/httemplate/elements/random_pass.html
+++ b/httemplate/elements/random_pass.html
@@ -1,13 +1,23 @@
 <INPUT TYPE="button" VALUE="<% emt($label) %>" onclick="randomPass()">
 <SCRIPT TYPE="text/javascript">
 function randomPass() {
+  var lower='<% join('', 'a'..'z') %>';
+  var upper='<% join('', 'A'..'Z') %>';
+  var number='<% join('', '0'..'9') %>';
+  var symbol='`~!@#$%^&*-_=+:;<>,.?';
+  var pw_set=lower+upper+number+symbol;
+  var pass=[];
+  pass.push(lower.charAt(Math.floor(Math.random() * lower.length)));
+  pass.push(upper.charAt(Math.floor(Math.random() * lower.length)));
+  pass.push(number.charAt(Math.floor(Math.random() * number.length)));
+  pass.push(symbol.charAt(Math.floor(Math.random() * symbol.length)));
   var i=0;
-  var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>';
-  var pass='';
-  while(i < 8) {
+  while(i < 4) {
     i++;
-    pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length));
+    pass.push(pw_set.charAt(Math.floor(Math.random() * pw_set.length)));
   }
+  for(var j, x, i = pass.length; i; j = Math.floor(Math.random() * i), x = pass[--i], pass[i] = pass[j], pass[j] = x);
+  pass = pass.join('');
   document.getElementById('<% $id %>').value = pass;
 }
 </SCRIPT>
diff --git a/httemplate/elements/validate_password.html b/httemplate/elements/validate_password.html
new file mode 100644
index 0000000..fd2cb6c
--- /dev/null
+++ b/httemplate/elements/validate_password.html
@@ -0,0 +1,58 @@
+<%doc>
+
+To validate passwords via javascript/xmlhttp:
+
+  <INPUT ID="password_field" TYPE="text">
+  <DIV ID="password_field_result">
+  <& '/elements/validate_password.html', 
+     fieldid  => 'password_field',
+     svcnum   => $svcnum
+  &>
+
+The ID of the input field can be anything;  the ID of the DIV in which to display results
+should be the input id plus '_result'.
+
+</%doc>
+
+<& '/elements/xmlhttp.html',
+    'url'  => $p.'misc/xmlhttp-validate_password.html',
+    'subs' => [ 'validate_password' ],
+    'method' => 'POST', # important not to put passwords in url
+&>
+<SCRIPT>
+function add_password_validation (fieldid) {
+  var inputfield = document.getElementById(fieldid);
+  inputfield.onchange = function () {
+    var fieldid = this.id+'_result';
+    var resultfield = document.getElementById(fieldid);
+    if (this.value) {
+      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','password',this.value,
+        function (result) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(result.fieldid);
+          if (resultfield) {
+            if (result.valid) {
+              resultfield.innerHTML = '<SPAN STYLE="color: green;">Password valid!</SPAN>';
+            } else if (result.error) {
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+            } else {
+              result.syserror = result.syserror || 'Server error';
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+            }
+          }
+        }
+      );
+    } else {
+      resultfield.innerHTML = '';
+    }
+  };
+}
+add_password_validation('<% $opt{'fieldid'} %>');
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+</%init>
+
+
diff --git a/httemplate/misc/xmlhttp-validate_password.html b/httemplate/misc/xmlhttp-validate_password.html
new file mode 100644
index 0000000..28dbf64
--- /dev/null
+++ b/httemplate/misc/xmlhttp-validate_password.html
@@ -0,0 +1,50 @@
+<%doc>
+Requires cgi params 'password' (plaintext) and 'sub' ('validate_password' is only 
+acceptable value.)  Also accepts 'svcnum' (for svc_acct, will otherwise create an
+empty dummy svc_acct) and 'fieldid' (for html post-processing, passed along in 
+results for convenience.)
+
+Returns a json-encoded hashref with keys of 'valid' (set to 1 if object is valid),
+'error' (error text if password is invalid) or 'syserror' (error text if password
+could not be validated.)  Only one of these keys will be set.  Will also set
+'fieldid' if it was passed.
+</%doc>
+
+<% encode_json($result) %>
+
+<%init>
+
+my $validate_password = sub {
+  my %arg = $cgi->param('arg');
+  my %result;
+
+  $result{'fieldid'} = $arg{'fieldid'}
+    if $arg{'fieldid'} =~ /^\w+$/;
+
+  $result{'syserror'} = 'Request is not POST' unless $cgi->request_method eq 'POST';
+  return \%result if $result{'syserror'};
+
+  my $password = $arg{'password'};
+  $result{'syserror'} = 'Invoked without password' unless $password;
+  return \%result if $result{'syserror'};
+
+  my $svcnum = $arg{'svcnum'};
+  $result{'syserror'} = 'Invalid svcnum' unless $svcnum =~ /^\d*$/;
+  return \%result if $result{'syserror'};
+
+  my $svc_acct = $svcnum 
+    ? qsearchs('svc_acct',{'svcnum' => $svcnum})
+    : (new FS::svc_acct {});
+  $result{'syserror'} = 'Could not find service' unless $svc_acct;
+  return \%result if $result{'syserror'};
+
+  $result{'error'} = $svc_acct->is_password_allowed($password);
+  $result{'valid'} = 1 unless $result{'error'};
+  return \%result;
+};
+
+my $result = ($cgi->param('sub') eq 'validate_password')
+             ? &$validate_password()
+             : { 'syserror' => 'Invalid sub' };
+
+</%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/ClientAPI/MyAccount.pm                       |   45 ++++++++++--
 FS/FS/Password_Mixin.pm                            |   74 +++++++++++++++++++-
 FS/FS/svc_acct.pm                                  |   19 +++++
 debian/control                                     |    5 +-
 fs_selfservice/FS-SelfService/SelfService.pm       |    1 +
 .../FS-SelfService/cgi/add_password_validation.js  |   38 ++++++++++
 .../FS-SelfService/cgi/change_password.html        |   12 +++-
 .../FS-SelfService/cgi}/images/error.png           |  Bin 666 -> 666 bytes
 .../FS-SelfService/cgi}/images/tick.png            |  Bin 537 -> 537 bytes
 .../cgi/process_forgot_password.html               |   19 ++++-
 fs_selfservice/FS-SelfService/cgi/selfservice.cgi  |   20 +++++-
 fs_selfservice/FS-SelfService/cgi/send_xmlhttp.js  |   43 ++++++++++++
 fs_selfservice/FS-SelfService/cgi/signup.html      |   14 +++-
 .../FS-SelfService/cgi/validate_password.html      |    9 +++
 httemplate/edit/svc_acct.cgi                       |    7 +-
 httemplate/elements/change_password.html           |    8 ++-
 httemplate/elements/random_pass.html               |   18 +++--
 httemplate/elements/validate_password.html         |   60 ++++++++++++++++
 httemplate/misc/xmlhttp-validate_password.html     |   50 +++++++++++++
 19 files changed, 419 insertions(+), 23 deletions(-)
 create mode 100644 fs_selfservice/FS-SelfService/cgi/add_password_validation.js
 copy {httemplate => fs_selfservice/FS-SelfService/cgi}/images/error.png (100%)
 copy {httemplate => fs_selfservice/FS-SelfService/cgi}/images/tick.png (100%)
 create mode 100644 fs_selfservice/FS-SelfService/cgi/send_xmlhttp.js
 create mode 100644 fs_selfservice/FS-SelfService/cgi/validate_password.html
 create mode 100644 httemplate/elements/validate_password.html
 create mode 100644 httemplate/misc/xmlhttp-validate_password.html




More information about the freeside-commits mailing list