0

I have a PHPMailer form and I am using .ajax using this SO answer jquery-ajax-post-example-with-php

It is successfully sending or not sending, so I know the Mailer and captcha are working. Now, if I could get different behaviors for success and fail, that would be great, but I am not getting this right somehow.

PREFERRED BEHAVIORS

SUCCESS --> reload screen and show bootstrap modal with success msg

FAIL --> reload screen and show bootstrap modal with fail msg

I have a lot of code so, Think of the modals as vanilla as possible, and the PHPMailer is, in fact, sending as well. The problem I am having should be in the code below, but if you have to have something else, just ask. Trying to keep the question as decluttered as possible

  <script type="text/javascript">
        $(document).ready(function() {
            var request;

            $("#contactForm").submit(function(event){
                event.preventDefault();

                // Abort any pending request
                if (request) {
                    request.abort();
                }

                // setup some local variables
                var $form = $(this);

                // Let's select and cache all the fields
                var $inputs = $form.find("input, select, button, textarea");

                // Serialize the data in the form
                var serializedData = $form.serialize();

                // Let's disable the inputs for the duration of the Ajax request.
                // Note: we disable elements AFTER the form data has been serialized.
                // Disabled form elements will not be serialized.
                $inputs.prop("disabled", true);

                request = $.ajax({
                    url: "processLogin.php",
                    type: "post",
                    data: serializedData
                });

                // Callback handler that will be called on success
                request.done(function (response, textStatus, jqXHR){
                    if (response == true ) {
                        top.location.reload();
                        // top.location.href="/";
                        // $('#successEmail').modal('show');
                    } else {
                        // top.location.reload();
                        $('#failEmail').modal('show');
                    }
                });

                // Callback handler that will be called on failure
                request.fail(function (jqXHR, textStatus, errorThrown){
                    // Log the error to the console
                    // console.error(
                    //     "The following error occurred: "+
                    //     textStatus, errorThrown
                    // );
                    // top.location.reload();
                });

                // Callback handler that will be called regardless
                // if the request failed or succeeded
                request.always(function () {
                    // Reenable the inputs
                    $inputs.prop("disabled", false);
                });

            });
        });
    </script>

PHPMailer Form

<?php
    session_start();
?>
<?php
    /**
    * This example shows how to handle a simple contact form.
    */
    $msg = '';


    use PHPMailer\PHPMailer\PHPMailer;

    require './mail/PHPMailer.php';
    require './mail/Exception.php';
    require './mail/SMTP.php';
    require './mail/PHPMailerAutoload.php';

    include_once $_SERVER['DOCUMENT_ROOT'] . '/securimage/securimage.php';

    $securimage = new Securimage();

    //Don't run this unless we're handling a form submission
    if (array_key_exists('email', $_POST)) {
        date_default_timezone_set('Etc/UTC');

        //Create a new PHPMailer instance
        $mail = new PHPMailer;

        $mail->SMTPDebug = 0;                                 
        $mail->isSMTP();    
        $mail->Host = 'smtp.live.com';                 
        $mail->SMTPAuth = true;     
        $mail->Username = 'email@outlook.com';       
        $mail->Password = 'password';                    
        $mail->SMTPSecure = 'tls';                   
        $mail->Port = 587;        

        $mail->setFrom('email@outlook.com', 'Mailer');
        $mail->addAddress('email@outlook.com', 'First Last');


        $email = isset($_POST['email']) ? $_POST['email'] : null;
        $name = isset($_POST['name']) ? $_POST['name'] : null;
        $phone = isset($_POST['phone']) ? $_POST['phone'] : null;
        $message = isset($_POST['message']) ? $_POST['message'] : null;


        if ($mail->addReplyTo($_POST['email'], $_POST['name'])) {
            $mail->Subject = 'Contact request';
            $mail->isHTML(true);
            $mail->Body = <<<EOT
    <div style="width:100%">
    <div><label style="color: #044F69; font-weight:bold">Email:</label> <span>{$_POST['email']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Name:</label> <span>{$_POST['name']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Phone:</label> <span>{$_POST['phone']}</span></div>
    <div><label style="color: #044F69; font-weight:bold">Message:</label> <span>{$_POST['message']}</span></div>
    </div>
    EOT;

            if ($securimage->check($_POST['captcha_code']) == false) {
                // echo "<meta http-equiv='refresh' content='0'>";
                exit;
            }

            //Send the message, check for errors
            if (!$mail->send()) {
                $msg = 'Sorry, something went wrong. Please try again later.';
            } else {
                header("Location: /");
                exit;
            }
        } else {
            $msg = 'Invalid email address, message ignored.';
        }
    }
    ?>

P.S. The code as I have it (See above), shows the fail modal and no reload regardless of whether it passes or fail.

Thanks in advance!

LOTUSMS
  • 10,317
  • 15
  • 71
  • 140
  • response will be a string not a boolean. Show what you actually echo server side – charlietfl Dec 08 '17 at 16:43
  • @charlietfl I edited my question. Thanks – LOTUSMS Dec 08 '17 at 17:02
  • Redirect server side won't cause page to redirect and your message has nothing to do with `true` – charlietfl Dec 08 '17 at 17:07
  • @charlietfl I'm not sure I follow what you're saying. Can you elaborate? What am I missing? – LOTUSMS Dec 08 '17 at 17:11
  • These two pieces of code simply don't fit together. Make your PHPMailer code work first, then make it produce the right output to keep your JS happy. It's all very confused as you've presented it. If you're using PHPMailer 6, the autoloader doesn't exist any more. – Synchro Dec 08 '17 at 21:50
  • @Synchro If only there was a website that offered a good tutorial on how to do this. Trust me, I would. Everything I've found it's bits and pieces. Even the tutorials in Khan and Udemy are outdated and incomplete – LOTUSMS Dec 08 '17 at 21:55
  • There are plenty of tutorials on both, you just need to apply a little consistency and logic. You need your PHP script to return HTTP response codes that your javascript will interpret as success or failure. At the moment you're either returning a success code (200) or a redirect (301) that means nothing to JS. – Synchro Dec 09 '17 at 07:12

3 Answers3

2

You've chosen a relatively difficult example to learn with. Reloading after success/failure means you need to introduce sessions or cookies or URL parameters to track state between requests. There are several moving parts you need to get right. You must have your reasons for doing that, but if you can get away without the reload it simplifies things and would probably be a better user experience. I've included info below for both options.

First up, the PHP. You have several possible exit states, depending on whether the email was successfully sent etc, but they are handled inconsistently. In one case it does a redirect, in some cases it exits silently. All cases should be consistent; the request to that PHP page should generate a response, no matter what happens, and ideally pass that response back to your calling Javascript so it can act on whatever happened.

It looks like you started setting that up, but didn't complete it - you prepare a $msg with info about what happened, but that is never displayed, and $msg is missing completely from a few exit cases (specifically the success case and what looks like a CAPTCHA failure).

In addition to your $msg, I'd include a status flag so the JS can easily tell whether the request was successful or not; and since you're reading this response in JS, I'd make it JSON.

Note that as you want to reload the page in both success/failure cases, strictly speaking a JSON response isn't really necessary, but I think it is good practice and will help with debugging while developing.

To make sure each exit condition generates a standard response, I've had to nest the actual mail sending code inside the CAPTCHA test. This IMO is a bit messy, and personally I'd restructure things, returning early to make testing the various conditions simpler and so you don't end up with 3-4 nested tests. For example instead of nesting everything inside the array_key_exists('email', $_POST) test, test the inverse (if there is no email value) and exit immediately if it fails.

// Don't run this unless we're handling a form submission
if (!array_key_exists('email', $_POST)) {
    // Generate response and bail out!
    echo $some_response_or_whatever;
    die();
}

I'd also suggest perhaps rethinking the flow, eg if CAPTCHA fails, do you really want to have to reload just to show that error? You're sabotaging one of the niceties of using AJAX - getting responses without having to reload. Maybe if something fails, you can just show the error msg on the form where the user clicked submit? If it succeeds, you open your modal, with a reload if really necessary.

Summary of the changes I made below:

  1. Nest $mail->send() inside CAPTCHA condition;

  2. Create a $response with appropriate message for CAPTCHA failure;

  3. Create a $response for success case;

  4. Add status flag to all responses;

  5. Add the $response to the session, so that it is available after page reload;

  6. Finally, echo the response as JSON so the calling AJAX can tell what happened.

Here's the updated, abbreviated (I removed large chunks of unchanged code) code:

<?php
session_start();

// ... your code

//Don't run this unless we're handling a form submission
if (array_key_exists('email', $_POST)) {

    // ... your code

    if ($mail->addReplyTo($_POST['email'], $_POST['name'])) {

        // ... your code

        if ($securimage->check($_POST['captcha_code']) == false) {
            // Generate a response in this failure case, including a message and a status flag
            $response = [
                'status'=> 1,
                'msg'   => 'CAPTCHA test failed!'
            ];

        } else {
            //Send the message, check for errors
            if (!$mail->send()) {
                // Generate a response in this failure case, including a message and a status flag
                $response = [
                    'status'=> 1,
                    'msg'   => 'Sorry, something went wrong. Please try again later.'
                ];
            } else {
                // Generate a response in the success case, including a message and a status flag
                $response = [
                    'status'=> 0,
                    'msg'   => 'Success!'
                ];
            }
        }
    } else {
        // Generate a response in this failure case, including a message and a status flag
        $response = [
            'status'=> 1,
            'msg'   => 'Invalid email address, message ignored.'
        ];
    }
}

// Add the response to the session, so that it will be available after reload
$_SESSION['response'] = $response;

// Finally display the response as JSON so calling JS can see what happened
header('Content-Type: application/json');
echo json_encode($response);
?>

Next, your Javascript. Since you want the page to reload in both success and failure cases, you don't really need to test the response.

request.done(function (response, textStatus, jqXHR){
    // You could test the response here, but since both success and failure 
    // should reload, there is no point.
    window.location.reload();

    // If you did want to test response and act on it, here's how to do that:
    /*
    if (response.status == 0) {
        // Success! Do something successful, like show success modal
        $('#successEmail').modal('show');
    } else {
        // Oh no, failure - notify the user
        $('.message').html(response.msg);
    }
    */
});

You did not mention in your description what should happen in the fail case. Note the fail callback is fired when the request fails, eg 404 or timeout or something like that, not when one of your failure cases like invalid email is triggered. I'd simply generate a msg on the front end in this case, without reloading:

request.fail(function (jqXHR, textStatus, errorThrown){
    // Say you have a <div class="message"></div> somewhere, maybe under your form
    $('.message').html('Error: ' + textStatus + ', ' + errorThrown)
});

So, now your PHP has fired, set a session variable and returned, and your JS has reloaded the page. You need to test the content of your session variable, and fire some JS accordingly. On your front end page, inside your existing $(document).ready( ...:

<script>
$(document).ready(function() {
    var request;

    // ... your code 

    <?php 
    // New PHP/JS to handle what happens when the page reloads
    if (isset($_SESSION['response'])) { 
        if ($_SESSION['response']['status'] == 0) { ?>
            // Success!
            $('#successEmail').modal('show');
        <?php } else { ?>
            $('#failEmail').modal('show');
        <?php }

        // In both cases we need to clear the response so next reload does not
        // fire a modal again
        unset($_SESSION['response']);
    } ?>
});
</script>

You can access the content of the success/failure message in your modals using $_SESSION['response']['msg'].

Another suggestion - if your success and failure modals are similar, you could use just one, and update its content with Javascript.

<script>
<?php if (isset($_SESSION['response'])) { ?>
    // First add the message to the modal, assuming you have a 
    // <div class="message"></div> in your modal
    $('#modal .message').html('<?php echo $_SESSION['response']['msg']; ?>');

    // Now open the one modal to rule them all
    $('#modal').modal('show');

    <?php
    unset($_SESSION['response']);
} ?>
</script>

Side note - I am not sure if your Abort any pending request is really doing what you want. request is declared on document ready, so it is always going to exist, AFAICT?

Don't Panic
  • 13,965
  • 5
  • 32
  • 51
  • Incredibly documented! Thank you! I understood everything you said. Except the last code that goes on the front end. I tried either one and got and error for `Parse error: syntax error, unexpected '{' in filename on line 167` which is ``...am I placing this in the wrong place? `` , ``, etc? – LOTUSMS Dec 12 '17 at 02:04
  • If I documented, I get a refresh and the mails sent as instructed. So I am assuming that is an indication that it all works. The modals are the ones that I can't get to show because I can't get that particular part of your code right – LOTUSMS Dec 12 '17 at 02:05
  • Also your code for the `request.done` seems to be working fine if un-comment the commented lines and use that approach except both, the modal and the error message, stay on the screen for a second maybe and then it goes away. If there was a way to delay that by 5 secs for example, it can be an acceptable solution as well. – LOTUSMS Dec 12 '17 at 02:18
  • @LOTUSMS RE: `Parse error` - sorry, typo, both examples were missing a `)`, fixed. Pls try again and let me know how you go. – Don't Panic Dec 12 '17 at 03:20
  • @LOTUSMS RE: modals disappearing, could it be that you've uncommented the extra stuff in `request.done`, but left the `window.location.reload();` there? That would result in modals opening but page reloading at the same time, so the modals would disappear quickly. You should use one or the other of those approaches - either reload, and let one of the final bits of SESSION related code open the modals, *OR* don't reload, and show your modals/messages right there in `request.done`. Does that make sense? – Don't Panic Dec 12 '17 at 03:29
  • The error is gone! That's' good, but no modals. That's bad because there is no indication that the message was actually sent. I'm using the one with different modals. – LOTUSMS Dec 12 '17 at 03:43
  • @LOTUSMS OK, anything on the console (browser devtools, console tab)? Probably that SESSION code needs to go inside your `$(document).ready`, otherwise jQuery and Bootstrap won't have loaded and `.modal()` won't exist yet. I've updated my answer to clarify where to put it. – Don't Panic Dec 12 '17 at 03:51
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/160988/discussion-between-dont-panic-and-lotusms). – Don't Panic Dec 12 '17 at 04:03
  • Alright, I ended up using the request.done logic. Served the modals for each condition met. The failEmail calls the fail modal to display a message and instructions. No refresh. Once the form is corrected and it passes, it serves the successEmail modal with message and a passed the reload form to the Close button on click so that the page refreshes. – LOTUSMS Dec 12 '17 at 04:23
  • Thanks a lot for your help and most of all for the education. – LOTUSMS Dec 12 '17 at 04:24
  • @LOTUSMS Glad to help. If you've gone with a non-reloading solution, you can get rid of all the SESSION stuff, so when you come back to it in 6 months time you won't wonder what it is doing :-) – Don't Panic Dec 12 '17 at 07:57
0

Your callback function for done should only reflect the success condition, while the fail should reflect the alternative.

// Callback handler that will be called on success
request.done(function(response, textStatus, jqXHR) {
  $('#successEmail').modal('show');
});

// Callback handler that will be called on failure
request.fail(function(jqXHR, textStatus, errorThrown) {
  // Log the error to the console
  // console.error(
  //     "The following error occurred: "+
  //     textStatus, errorThrown
  // );
  $("#failEmail").modal("show");
});

As provided/described in your question, reloading the page would prevent the script from getting to show either modal- if you need to reload, you should store a localStorage variable or cookie and test its value on page load to show modal as necessary.

Anson W Han
  • 409
  • 2
  • 7
  • Didn't really work. I think I had tried this before. It showed the success msg on both pass and fail and didn't reload the page either, so the form data remained in the fields as well – LOTUSMS Dec 08 '17 at 17:01
  • Requires sending a status from server to trigger the fail – charlietfl Dec 08 '17 at 17:05
  • once you have updated your php to return a status in the headers, you can also try changing your initial submit function to be of the format: $("#contactForm").on('submit',function(event){ ... }); You may be inadvertently triggering the form to submit on page load, and since nothing is populated, it would fail to populate email data to send succcesfully. – Anson W Han Dec 08 '17 at 17:06
  • How do I do this? What do you mean by status in the headers? – LOTUSMS Dec 08 '17 at 17:14
  • @AnsonWHan I don't think it's sending on page load. If I remove the ajax and set `header("Location: /");` on `$mail --> send()` it sends it only after it has gone through the POST checks. I had it that way before but with captcha, I need a more intuitive way to let the user know they missed the captcha. – LOTUSMS Dec 08 '17 at 17:19
  • In your PHP, you can replace your call for php to do the reload/redirection `header("Location: /");` to send an http_response_code instead using `return http_response_code(xxx);`, where xxx is the desired code; and have the ajax callback in javascript refresh if necessary. Refer to http://php.net/manual/en/function.http-response-code.php – Anson W Han Dec 09 '17 at 03:26
0

I haven't done PHP in quite a while, but I believe the code below will solve your problems. Here is what I would change your PHP code to:

<?php 
   $messageSent = true;
    try {
        $mail -> send();
    }
    catch (Exception $e) {
        $messageSent = false
    }
    $response = array('success' => $messageSent);
    echo json_encode($response)

Now in your jQuery code, use an $.always() callback instead of success:

    request.always(function (data) {
        if (data.success) {
          // success handler    
        } else {
           // error handler
        } 
    });

In your code, you have the following PHP:

if (!$mail->send()) {}

I don't think that line will ever execute. You are assuming that $mail->send() returns false if the message does not send. I do not think this is the case. If you look at the example used on the PHP mailer home page: https://github.com/PHPMailer/PHPMailer The author uses a try / catch. This tells me that if the message fails to send, PHPMailer throws an exception, which if uncaught will propagate a 500 error. I believe that your PHPMailer setup has not thrown any exceptions (yet), which is why your Ajax success handler is always called. However, in the following line of your javascript: if (response == true ) { you have incorrectly assumed that the first argument sent to your success handler is boolean (true or false). It is not (although we can make it so). Therefore, (response == true) will never evaluate to true.

What I'm doing in my code is using a try / catch like the PHPMailer example does. This way your backend won't throw a 500 error if PHPMailer fails. However, if the catch clause executes, I am setting a boolean flag to false. This enables us to know that there was a failure without actually having your backend crash. Now we simply create a JSON object with a key called success. This will be true if the message was successfully sent, and false if it was not. Now we can return that to the front-end.

Since we are catching any possible exceptions on the backend, the frontend will always receive an HTTP 200 status code (okay). Therefore, there is no sense in using .success() or .error() as jQuery will not know based on HTTP code whether we succeeded or failed. Instead, we use an .always() callback and examine the value of the JSON object returned to determine success or error. Now we can simply use the if statement inside of the .always() handler to execute the success code ("if") or the error code ("else"). Anything that you wish to do with respect to showing or hiding modals, etc. can be done in JavaScript on the front-end, which is where that code belongs.

I hope this helps.

Neil Girardi
  • 4,533
  • 1
  • 28
  • 45