Ajax form, serverside script seems to run twice per submit
MatthewHSE
Status: Contributor
Joined: 20 Jul 2004
Posts: 122
Location: Central Illinois, typically glued to a computer screen
Reply Quote
Here's the culmination of the project that has prompted my past few threads here. I'm working on a newsletter subscription form to be submitted with Ajax, processed serverside, and results returned to the page with Ajax. I have it all pretty much figured out, except for some really strange problems with the duplicate-entry checking. Somehow, the serverside script seems to run twice under some circumstances.

To test this, you'll need to create two files, as shown below:

test.html:
:: Code ::
<html>
<head>
<title>Ajax Test</title>
<script type="text/javascript">
// Ajax form submission
function submitForm()
{
  var http = null;
  if(window.XMLHttpRequest)
    http = new XMLHttpRequest();
  else if (window.ActiveXObject)
    http = new ActiveXObject("Microsoft.XMLHTTP");

  http.onreadystatechange = function()
  {
    if(http.readyState == 4){
        var response = http.responseText;
        var update = new Array();
        if(response.indexOf('|' != -1)) {
            update = response.split('|');
            document.getElementById(update[0]).innerHTML = update[1];
        }
    }
  }

  http.open('POST', '/subscribe.php', true);
  http.setRequestHeader('Content-Type',
    'application/x-www-form-urlencoded');
  http.send("email=" + document.getElementById('email').value);
}
</script>
</head>
<body>
<script type="text/javascript">
document.write('<h1>Free Newsletter!</h1>');
document.write('<form name="subscribe" action="javascript: submitForm();">');
document.write('<div id="newsletter">');
document.write('<input type="text" size="18" name="email" id="email" value="E-mail address">');
document.write('<input type="submit" value="Subscribe" onclick="submitForm();">');
document.write('</div>');
document.write('</form>');
</script>
</body>
</html>


And the serverside processing script, subscribe.php, which is probably as inefficient as all get out, but then I still haven't been with PHP for long:

:: Code ::
<?php

$email = $_POST['email'];

$form = '<input type="text" size="18" name="email" id="email" value="E-mail address"> <input type="submit" value="Subscribe" onclick="submitForm();"><br>';

// Checking e-mail address
if (strlen($email) > 0) // If data has been entered...
  {
    if (!check_email($email)) // Check first if it's correctly formatted.
      {
        echo "newsletter|$form Your e-mail address appears to be in an incorrect format. Please re-enter it above and try again.";
        exit;
      }
    else  // If data has been entered and it passes the formatting test above, the e-mail address is okay.
      {
        // Check to see if they're already subscribed
        $addresses         = file_get_contents("/path/to/list.txt"); // The "real" list
        $addressesdupe     = eregi($email, $addresses, $check);
        $addressestemp     = file_get_contents("/path/to/temp.txt"); // The temporary list for new subscribers
        $addressestempdupe = eregi($email, $addressestemp, $checktemp);
        if (isset($check[0]) || isset($checktemp[0]))
          {
            // The e-mail address is already in either the real or the temporary list
            echo "newsletter|That e-mail address is already subscribed to our newsletter. If you have not been receiving our mailings, please <a href=\"/support/\">contact support</a> for assistance.";
            exit;
          }
        else
          {
            $file = '/path/to/temp.txt';
            // Try to open the file...
            if (!$handle = fopen ($file, 'a'))
              {
                // If opening the file fails, print an error message:
                echo "newsletter|An error has occurred. Please <a href=\"/support/\">contact support</a> and request to be added to our newsletter.";
                exit;
              }
            else
              {
                // We opened the file successfully, now let's write to it
                $email = "$email\n";
                if (fwrite($handle, $email) === FALSE)
                  {
                    // If we can't write to the file, print an error message:
                    echo "newsletter|An error has occurred. Please <a href=\"/support/\">contact support</a> and request to be added to our newsletter.";
                    exit;
                  }
                else
                  {
                    // We opened the file and were able to write to it, so print a confirmation message.
                    echo "newsletter|Thank you for joining our e-mail newsletter! You will receive e-mail confirmation of your subscription within the next few minutes.";
                    // Add e-mail confirmation code here
                    exit;
                  }
                // Close the file
                fclose($handle);
                exit;
              }
          }
      }
  }
else // If no data has been entered...
  {
    echo "newsletter|$form Please enter your e-mail address in the space provided above.";
    exit;
  }
exit;


// Function to validate e-mail addresses - Courtesy of aFormMail
function check_email($email) {
    #characters allowed on name: 0-9a-Z-._ on host: 0-9a-Z-. on between: @
    if (!preg_match('/^[0-9a-zA-Z\.\-\_]+\@[0-9a-zA-Z\.\-]+$/', $email))
        return false;

    #must start or end with alpha or num
    if ( preg_match('/^[^0-9a-zA-Z]|[^0-9a-zA-Z]$/', $email))
        return false;

    #name must end with alpha or num
    if (!preg_match('/([0-9a-zA-Z_]{1})\@./',$email) )                   
        return false;

    #host must start with alpha or num
    if (!preg_match('/.\@([0-9a-zA-Z_]{1})/',$email) )                   
        return false;

    #pair .- or -. or -- or .. not allowed
    if ( preg_match('/.\.\-.|.\-\..|.\.\..|.\-\-./',$email) )
        return false;

    #pair ._ or -_ or _. or _- or __ not allowed
    if ( preg_match('/.\.\_.|.\-\_.|.\_\..|.\_\-.|.\_\_./',$email) )
        return false;

    #host must end with '.' plus 2-5 alpha for TopLevelDomain
    if (!preg_match('/\.([a-zA-Z]{2,5})$/',$email) )
        return false;

    return true;
}
// End function for e-mail validation

?>


Now create two more files, list.txt and temp.txt. Populate them with a few e-mail addresses each, but not all the same.

When you load test.html in the browser, enter an e-mail address in the form. Try it a few times with several e-mail addresses and watch what hapens.

On my server, about one third of the time, I can enter a valid e-mail address that has not been added yet, then submit the form. The confirmation message prints, but it's followed in a few seconds by the "Already in our list" error message. The only way I can see this happening is if the subscribe.php script is actually being run twice somehow, even though I only submitted the form once.

Other times, I'll seem to get a false positive - the "Already in our list" message comes up immediately, even if the e-mail address entered was *not* in the list at the time. I suspect this is just the same thing as described in the previous paragraph, only so fast I can't see the confirmation message before the error message is printed.

I'll probably be switching this all over to a database soon instead of the flat files, but I thought these problems were interesting enough to be investigated.

Just to explain the logic a little, the PHP script is meant to echo values in the following format:

:: Code ::
valuename|text to return


The javascript splits the output from the PHP script at the | symbol. Before the | is the ID of the page element you're working with. After the | is the content that will be written to the page element after the script processes the form data.

The PHP script checks to make sure data was entered in the form, printing the form and an error message if nothing was submitted. If data was submitted, the script validates the e-mail address. If it's invalid, the form is printed again with an error message. If data is submitted and validates successfully, the script checks to see if the e-mail address has already been entered in the "real" text file OR the temporary text file. If it's in either of those files, an error message is returned. If it's not in either of those files, a confirmation message is returned and the e-mail address is written to the temp file.
Back to top
techAdmin
Status: Site Admin
Joined: 26 Sep 2003
Posts: 3895
Location: East Coast, West Coast? I know it's one of them.
Reply Quote
It is submitting twice is my guess, you skipped some of the necessary steps about creating the javascript submission found in my post in this thread:

:: Code ::
document.write('<input type="submit" value="Subscribe" onclick="submitForm();">');


Follow the logic here: you shouldn't use 'submit', you should use 'button'. When you click 'submit', the form does two things. First, onclick, it fires the function. Second, it submits the form. Since form submit also fires the function, the php page gets called twice. You have to put in the explicit protection I made below, trust my stuff, protection is a huge part of programming.

This is my first guess of what's happening, I'm not clear on why it only happens sometimes, it won't happen if you don't click submit though I would guess.

Place an alert before the javascript that calls the post to php page.

Like this:

alert('submission takes place');

This will immediately tell you if two or one submissions are happening.

Take a close look at the logic of the javascript, notice how I put a flag in the function call telling it what part is calling it? And how it returns true or false? Those aren't optional.

:: Code ::
<script>
// which determines if it's a link/onclick event or not
function doit(which)
{
alert('I did it');
document.getElementById('test').value = 'yippee';
if(which!='link'){return false;}
}
</script>
<form action='one.htm' onsubmit='return doit("submit");' >
<input type='text' name='test' id='test'>
<input type='button' value='ok' onclick='javascript:doit("link");'>
<a href='javascript:doit("link");'>submit</a>
</form>


Notice on your javascript, when you click the submit button, you fire the form, but it also submits the form. I'm not 100% sure that's the issue, but I think it is.

Placing an alert message easily allows you to see if the dual submission is happening on the javascript page or the php page. You want to eliminate the javascrpt page first before debugging the php. One of the php || statements may be incorrrect but I'm not positive about that.

Use alerts, place them at each key step of the javascript, make sure you can track the actual execution, especially when you get it to double submit.

There is essentially no protection on either the javascript or the php though.
Back to top
MatthewHSE
Status: Contributor
Joined: 20 Jul 2004
Posts: 122
Location: Central Illinois, typically glued to a computer screen
Reply Quote
Thanks for the tips. Believe me, I do trust your stuff, pretty much implicitly. The only reason I skipped steps in this case was because I couldn't figure out how to work your doit(which) function into the submitForm() function I already had. I need to look closer at what's going on and see if I can make it all work together, I guess. For now, I was at least able to make it work right by removing the onclick() from the submit button. I'm still going to look at your code and see how I can fit everything together.

Incidentally, what do you mean by "protection" in programming? Is that data validation, fault tolerance, or something else? I probably don't know enough about Javascript yet to understand an explanation of how to work protection into it, but if you could give some PHP pointers, I'd really appreciate it.

What all technologies do you know, anyway? So far I'm not aware of anything web-related or hardware-related that you're not pretty well acquainted with. I'd love to hear something about how you got into programming, how you started learning, principles you use to pick up on new techniques, etc.
Back to top
techAdmin
Status: Site Admin
Joined: 26 Sep 2003
Posts: 3895
Location: East Coast, West Coast? I know it's one of them.
Reply Quote
This should more or less work, I added javascript client side form validation, tested for submit/button, and a few other things.

This is your code and field names I think unless I missed something. Note that I haven't tested the actual ajax functionality of this, that might change things slighly, I don't know for sure. Easiest to do is just test this directly in your page, if it works it should just plug and play, if it doesn't there is an ajax logic error on my end. I'm not super clear on ajax though, in terms of how data passes back and forth on the page.

:: Code ::
<script type="text/javascript">
// Ajax form submission
function submitForm(which)
{
   // check to see if there is form data
   // this is also where you should do client side validation of email, with a
   // validation function, simple.
   if ( isvalid() )
   {
      var http = null;
      if(window.XMLHttpRequest)
      {
         http = new XMLHttpRequest();
      }
      else if (window.ActiveXObject)
      {
         http = new ActiveXObject("Microsoft.XMLHTTP");
      }

      http.onreadystatechange = function()
      {
         if(http.readyState == 4)
         {
            var response = http.responseText;
            var update = new Array();
            if(response.indexOf('|' != -1))
            {
               update = response.split('|');
               document.getElementById(update[0]).innerHTML = update[1];
            }
         }
      }

      http.open('POST', '/subscribe.php', true);
      // make sure to keep this on one line::
      http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
      http.send("email=" + document.getElementById('email').value);

      // here we return false so that the form doesn't actually submit to one.html
      // when the action that activates this is a form submission, hitting enter that is
      // this isn't strictly necessary I guess, you could just have it return false
      if( which == 'submit' ){return false;}
   }
   // if no form data, return false, do nothing in other words.
   else
   {
      return false;
   }
}
// let's clean up the form stuff a bit, note change from type=submit to type=button
// also note that we are using the correct 'onsubmit='return submitForm' in <form...
// replace 'one.htm' with your page file name
subscribe_form = '<h1>Free Newsletter!</h1>' +
'<form name="subscribe" action="one.htm" onsubmit="return submitForm(\'submit\')">' +
'<div id="newsletter">' +
'<input type="text" size="18" name="email" id="email" value="E-mail address">' +
'<input type="button" value="Subscribe" onclick="submitForm(\'link\');">' +
'</div></form>';

// form validation:
// note, older browsers don't recognize getelementbyid so we'll use the name value
function isvalid() {
   email=document.subscribe.email;
   blankemail='Please Fill in your Email Address.\n';
   at='Email addresses must include the \@ character.\n';
   period='Email addresses must include a Period.\n';
   message='The following information \nneeds to be completed:\n\n';
   var bUndone=true, bEmail;

   if (email.value == ' ') {email.value='';}

   if (email.value == '') {
      message += blankemail;
      bUndone = false;
      bEmail = true;
   }
   if (email.value != ''){
      if (email.value.indexOf('@') == -1){
         message += at;
         bUndone = false;
         bEmail = true;
      }
      if (email.value.indexOf('.') == -1){
         message += period;
         bUndone = false;
         bEmail = true;
      }
   }
   if (!bUndone){
      alert(message);
      if (bEmail) {email.focus();}
      return false;
   }
   else
   {
      return true;
   }
}
</script>
</head>
<body>
<script type="text/javascript">
// shoot me, I don't like multiple document.writes, or in php multiple echoes, put it
// all in one variable then write it out, much more efficient.
document.write( subscribe_form );
</script>


For the other questions, start a new thread if you want with those and I'll answer the best I can.
Back to top
Display posts from previous:   

All times are GMT - 8 Hours