© David Powers 2019
David PowersPHP 7 Solutionshttps://doi.org/10.1007/978-1-4842-4338-1_6

6. Bringing Forms to Life

David Powers1 
(1)
London, UK
 

Forms lie at the very heart of working with PHP. You use forms for logging in to restricted pages, registering new users, placing orders with online stores, entering and updating information in a database, sending feedback … and the list goes on. The same principles lie behind all these uses, so the knowledge you gain from this chapter will have practical value in most PHP applications. To demonstrate how to process information from a form, I’m going to show you how to gather feedback from visitors to your site and send it to your mailbox.

Unfortunately, user input can expose your site to malicious attacks. It’s important to check data submitted from a form before accepting it. Although HTML5 form elements validate user input in modern browsers, you still need to check the data on the server. HTML5 validation helps legitimate users avoid submitting a form with errors, but malicious users can easily sidestep checks performed in the browser. Server-side validation is not optional, but essential. The PHP solutions in this chapter show you how to filter out or block anything suspicious or dangerous. No online application is completely hack-proof, but it doesn’t take a lot of effort to keep all but the most determined marauders at bay. It’s also a good idea to preserve user input and redisplay it if the form is incomplete or errors are discovered.

These solutions build a complete mail-processing script that can be reused in different forms, so it’s important to read them in sequence.

In this chapter, you’ll learn about the following:
  • Understanding how user input is transmitted from an online form

  • Displaying errors without losing user input

  • Validating user input

  • Sending user input by email

How PHP Gathers Information from a Form

Although HTML contains all the necessary tags to construct a form, it doesn’t provide any means to process the form when submitted. For that, you need a server-side solution, such as PHP.

The Japan Journey web site contains a simple feedback form (see Figure 6-1). Other elements—such as radio buttons, check boxes, and drop-down menus—will be added later.
../images/332054_4_En_6_Chapter/332054_4_En_6_Fig1_HTML.jpg
Figure 6-1.

Processing a feedback form is one of the most popular uses of PHP

First, let’s take a look at the HTML code for the form (it’s in contact_01.php in the ch06 folder):
<form method="post" action="">
    <p>
        <label for="name">Name:</label>
        <input name="name" id="name" type="text">
    </p>
    <p>
        <label for="email">Email:</label>
        <input name="email" id="email" type="text">
    </p>
    <p>
        <label for="comments">Comments:</label>
        <textarea name="comments" id="comments"></textarea>
    </p>
    <p>
        <input name="send" type="submit" value="Send message">
    </p>
</form>

The first two <input> tags and the <textarea> tag contain both name and id attributes set to the same value. The reason for this duplication is accessibility. HTML uses the id attribute to associate the <label> element with the correct <input> element. Form-processing scripts, however, rely on the name attribute. So, although the id attribute is optional in the Submit button, you must use the name attribute for each form element that you want to be processed.

Note

The name attribute of a form input element shouldn’t normally contain spaces. If you want to combine multiple words, join them with an underscore (PHP will do this automatically if you leave any spaces). Because the script developed later in this chapter converts the name attributes to PHP variables, don’t use hyphens or any other characters that are invalid in PHP variable names.

Two other things to notice are the method and action attributes inside the opening <form> tag. The method attribute determines how the form sends data. It can be set to either post or get. The action attribute tells the browser where to send the data for processing when the Submit button is clicked. If the value is left empty, as here, the page attempts to process the form itself. However, an empty action attribute is invalid in HTML5, so that will need to be fixed.

Note

I have deliberately avoided using any of the new HTML5 form features, such as type="email" and the required attribute. This makes it easier to test the PHP server-side validation scripts. After testing, you can update your forms to use the HTML5 validation features. Validation in the browser is mainly a courtesy to the user to prevent incomplete information from being submitted, so it’s optional. Server-side validation should never be skipped.

Understanding the difference between post and get

The best way to demonstrate the difference between the post and get methods is with a real form. If you completed the previous chapter, you can continue working with the same files.

Otherwise, the ch06 folder contains a complete set of files for the Japan Journey site with all the code from Chapter 5 incorporated in them. Copy contact_01.php to the site root and rename it contact.php. Also copy footer.php, menu.php, and title.php of the ch06/includes folder to the includes folder in the site root.
  1. 1.

    Locate the opening <form> tag in contact.php and change the value of the method attribute from post to get, like this:

    <form method="get" action="">
     
  2. 2.

    Save contact.php and load the page in a browser. Type your name, email address, and a short message into the form, then click Send message.

     
../images/332054_4_En_6_Chapter/332054_4_En_6_Figa_HTML.jpg
  1. 3.

    Look in the browser address bar. You should see the contents of the form attached to the end of the URL, like this:

    ../images/332054_4_En_6_Chapter/332054_4_En_6_Figb_HTML.jpg

    If you break up the URL, it looks like this:
    http://localhost/phpsols-4e/contact.php
    ?name=David
    &email=david%40example.com
    &comments=Greetings%21+%3A-%29
    &send=Send+message

    The data submitted by the form has been appended to the basic URL as a query string that begins with a question mark. The value from each field and the submit button is identified by the name attribute of the form element, followed by an equal sign and the submitted data. The data from each input element is separated by an ampersand (&). URLs cannot contain spaces or certain characters (such as an exclamation mark or a smiley), so the browser replaces spaces with + and encodes other characters as hexadecimal values, a process known as URL encoding (for a full list of values, see www.degraeve.com/reference/urlencoding.php ).

     
  2. 4.

    Go back into the code of contact.php and change method back to post, like this:

    <form method="post" action="">
     
  3. 5.

    Save contact.php and reload the page in your browser. Type another message and click Send message. Your message should disappear, but nothing else happens. It hasn’t been lost, but you haven’t done anything to process it yet.

     
  4. 6.
    In contact.php, add the following code immediately below the closing </form> tag:
    <pre>
    <?php if ($_POST) { print_r($_POST); } ?>
    </pre>

    This displays the contents of the $_POST superglobal array if any post data has been sent. As explained in Chapter 4, the print_r() function allows you to inspect the contents of arrays; the <pre> tags simply make the output easier to read.

     
  5. 7.

    Save the page and click the Refresh button in your browser. You’ll probably see a warning similar to the following. This tells you that the data will be resent, which is exactly what you want. Confirm that you want to send the information again.

     
../images/332054_4_En_6_Chapter/332054_4_En_6_Figc_HTML.jpg
  1. 8.

    The code from step 6 should now display the contents of your message below the form, as shown in Figure 6-2. Everything has been stored in one of PHP’s superglobal arrays, $_POST, which contains data sent using the post method. The name attribute of each form element is used as the array key, making it easy to retrieve the content.

     
../images/332054_4_En_6_Chapter/332054_4_En_6_Fig2_HTML.jpg
Figure 6-2.

The $_POST array uses the form’s name attributes to identify each element of data

As you have just seen, the get method sends your data appended to the URL, whereas the post method sends it with the HTTP headers so it’s hidden from view. Some browsers limit the maximum length of a URL to about 2,000 characters, so the get method can be used only for small amounts of data. The post method can be used for much larger amounts of data. By default, PHP permits up to 8 MB of post data, although hosting companies may set a different limit.

However, the most important difference between the two methods is their intended use. The get method is designed to be used for requests that result in no change on the server no matter how many times it’s made. Consequently, it’s used mainly for database searches; bookmarking your search result is useful because all the search criteria are in the URL. On the other hand, the post method is designed for requests that result in changes on the server. So, it’s used to insert, update, or delete records in a database, upload files, or send an email.

We’ll return to the get method later in the book. This chapter concentrates on the post method and its associated superglobal array, $_POST.

Getting form data with PHP superglobals

The $_POST superglobal array contains data sent using the post method. It should come as no surprise that data sent by the get method is in the $_GET array.

To access values submitted by a form, just put the name attribute of the form element in quotes between square brackets after $_POST or $_GET, depending on the form’s method attribute. So email becomes $_POST['email'] if sent by the post method, and $_GET['email'] if sent by the get method. That’s all there is to it.

You may come across scripts that use $_REQUEST, which avoids the need to distinguish between $_POST or $_GET. It’s less secure. You should always know where user information comes from. $_REQUEST also includes the values of cookies, so you have no idea if you’re dealing with a value submitted by the post method, or one transmitted through the URL, or injected by a cookie. Always use $_POST or $_GET.

Old scripts may use $HTTP_POST_VARS or $HTTP_GET_VARS, which have the same meaning as $_POST and $_GET. The old versions have been removed. Use $_POST and $_GET instead.

Processing and Validating User Input

The ultimate aim of this chapter is to send the input from the form in contact.php by email to your inbox. Using the PHP mail() function is relatively simple. It takes a minimum of three arguments: the address(es) the email is being sent to, a string containing the subject line, and a string containing the body of the message. You build the body of the message by concatenating (joining) the contents of the input fields into a single string.

Security measures implemented by most Internet service providers (ISPs) make it difficult if not impossible to test the mail() function in a local testing environment. Instead of jumping straight into the use of mail(), PHP Solutions 6-2 through 6-5 concentrate on validating user input to make sure required fields are filled in and displaying error messages. Implementing these measures makes your online forms more user-friendly and secure.

Using JavaScript or HTML5 form elements and attributes to check user input is called client-side validation because it happens on the user’s computer (or client). It’s useful because it’s almost instantaneous and can alert the user to a problem without making an unnecessary round trip to the server. However, client-side validation is easy to sidestep. All a malicious user has to do is to submit data from a custom script and your checks are rendered useless. It’s vital to check user input with PHP, too.

Tip

Client-side validation on its own is insufficient. Always verify the data from an external source using server-side validation with PHP.

Creating a reusable script

The ability to reuse the same script—perhaps with only a few edits—for multiple web sites is a great timesaver. However, sending the input data to a separate file for processing makes it difficult to alert users to errors without losing their input. To get around this problem, the approach taken in this chapter is to use what’s known as a self-processing form .

When the form is submitted, the page reloads and a conditional statement runs the processing script. If the server-side validation detects errors, the form can be redisplayed with error messages while preserving the user’s input. Parts of the script specific to the form will be embedded above the DOCTYPE declaration. Generic, reusable parts will be in a separate file that can be included in any page that requires an email-processing script.

PHP Solution 6-1: Preventing cross-site scripting in a self-processing form

Leaving the action attribute of an opening form tag empty or omitting it altogether reloads the form when the data is submitted. However, an empty action attribute is invalid in HTML5. PHP has a very convenient superglobal variable ($_SERVER['PHP_SELF']) that contains the site-root-relative path of the current file. Setting it as the value of the action attribute automatically inserts the correct value for a self-processing form—but using it on its own exposes your site to a malicious attack known as cross-site scripting (XSS). This PHP solution explains the risk and shows how to use $_SERVER['PHP_SELF'] securely.
  1. 1.

    Load bad_link.php in the ch06 folder into a browser. It contains a single link to form.php in the same folder; but the link in the underlying HTML has been deliberately malformed to simulate an XSS attack.

     
  2. 2.
    Click the link. Depending on which browser you’re using, you should either see that the target page has been blocked (as shown in Figure 6-3) or the JavaScript alert dialog shown in Figure 6-4.
    ../images/332054_4_En_6_Chapter/332054_4_En_6_Fig3_HTML.jpg
    Figure 6-3.

    Google Chrome automatically blocks suspected XSS attacks

    ../images/332054_4_En_6_Chapter/332054_4_En_6_Fig4_HTML.jpg
    Figure 6-4.

    Not all browsers are capable of blocking XSS attacks

     

Note

The links in the exercise files for this PHP solution assume that they are in a folder called phpsols-4e/ch06 in your localhost server root. Adjust them if necessary to match your testing setup.

  1. 3.

    Dismiss the JavaScript alert, if necessary, and right-click to view the page source. Line 10 should look similar to this:

    ../images/332054_4_En_6_Chapter/332054_4_En_6_Figd_HTML.jpg

    The malformed link in bad_link.php has injected a snippet of JavaScript into the page immediately after the opening <form> tag. In this instance, it’s a harmless JavaScript alert; but in a real XSS attack, it could try to steal cookies or other personal information. Such an attack would be silent, leaving the user unaware of what has happened unless they notice the script in the browser address bar.

    This has happened because form.php uses $_SERVER['PHP_SELF'] to generate the value of the action attribute. The malformed link inserts the page location in the action attribute, closes the opening form tag, and then injects the <script> tag, which is executed immediately as the page loads.

     
  2. 4.

    A simple, but effective, way to neutralize this type of XSS attack is to pass $_SERVER['PHP_SELF'] to the htmlentities() function like this:

    <form method="post"  action="<?= htmlentities($_SERVER['PHP_SELF']) ?>">

    This converts the angle brackets of the <script> tags to their HTML entity equivalents, preventing the script from being executed. Although it works, it leaves the malformed URL in the browser address bar, which could lead users to question the security of your site. I think a better solution is to redirect users to an error page when XSS is detected.

     
  3. 5.
    In form.php, create a PHP block above the DOCTYPE declaration, and define a variable with the site-root-relative path to the current file like this:
    <?php
    $currentPage = '/phpsols-4e/ch06/form.php';
    ?>
    <!doctype html>
     
  4. 6.
    Now compare the value of $currentPage with $_SERVER['PHP_SELF']. If they’re not identical, use the header() function to redirect the user to an error page and immediately exit the script like this:
    if ($currentPage !== $_SERVER['PHP_SELF']) {
        header('Location: http://localhost/phpsols-4e/ch06/missing.php');
        exit;
    }
     

Caution

The location passed to the header() function must be a fully qualified URL. If you use a document-relative link, the destination is appended to the malformed link, preventing the page from being redirected successfully.

  1. 7.

    Use $currentPage as the value of the action attribute in the opening form tag:

    <form method="post"  action="<?= $currentPage ?>">
     
  2. 8.

    Save form.php, return to bad_link.php, and click the link again. This time you should be taken directly to missing.php.

     
  3. 9.

    Load form.php directly in the browser. It should load and work as expected.

    The finished version is in form_end.php in the ch06 folder. The file called bad_link_end.php links to the finished version if you just want to test the script.

     

This technique involves more code than simply passing $_SERVER['PHP_SELF'] to the htmlentities() function; but it has the advantage of guiding users seamlessly to an error page if they have followed a malicious link to your form. Obviously, the error page should link back to your main menu.

PHP Solution 6-2: Making Sure Required Fields aren’t blank

When required fields are left blank, you don’t get the information you need and the user may never get a reply, particularly if contact details have been omitted.

Continue using the file from “Understanding the difference between post and get” earlier in this chapter. Alternatively, use contact_02.php from the ch06 folder and remove _02 from the filename.
  1. 1.
    The processing script uses two arrays called $errors and $missing to store details of errors and required fields that haven’t been filled in. These arrays will be used to control the display of error messages alongside the form labels. There won’t be any errors when the page first loads, so initialize $errors and $missing as empty arrays in the PHP code block at the top of contact.php, like this:
    <?php
    include './includes/title.php';
    $errors = [];
    $missing = [];
    ?>
     
  2. 2.
    The email-processing script should run only if the form has been submitted. Use a conditional statement to check the value of the superglobal variable $_SERVER['REQUEST_METHOD']. If it’s POST (all in uppercase), you know the form has been submitted using the post method. Add the code highlighted in bold to the PHP block at the top of the page.
    <?php
    include './includes/title.php';
    $errors = [];
    $missing = [];
    // check if the form has been submitted
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
        // email processing script
    }
    ?>
     

Tip

Checking that the value of $_SERVER['REQUEST_METHOD'] is POST is a generic condition that can be used with any form regardless of the name of the Submit button.

  1. 3.
    Although you won’t be sending the email just yet, define two variables to store the destination address and subject line of the email. The following code goes inside the conditional statement that you created in the previous step:
    if ( $_SERVER['REQUEST_METHOD'] == 'POST') {
        // email processing script
        $to = '[email protected]'; // use your own email address
        $subject = 'Feedback from Japan Journey';
    }
     
  2. 4.
    Next, create two arrays: one listing the name attribute of each field in the form and the other listing all required fields. For the sake of this demonstration, make the email field optional, so that only the name and comments fields are required. Add the following code inside the conditional block immediately after the code that defines the subject line:
        $subject = 'Feedback from Japan Journey';
        // list expected fields
        $expected = ['name', 'email', 'comments'];
        // set required fields
        $required = ['name', 'comments'];
    }
     

Tip

Why is the $expected array necessary? It’s to prevent an attacker from injecting other variables into the $_POST array in an attempt to overwrite your default values. By processing only those variables that you expect, your form is much more secure. Any spurious values are ignored.

  1. 5.
    The next section of code is not specific to this form, so it should go in an external file that can be included in any email-processing script. Create a new PHP file called processmail.php in the includes folder. Then include it in contact.php immediately after the code you entered in the previous step, like this:
        $required = ['name', 'comments'];
        require './includes/processmail.php';
    }
     
  2. 6.
    The code in processmail.php begins by checking the $_POST variables for required fields that have been left blank. Strip any default code inserted by your editor and add the following to processmail.php:
    <?php
    foreach ($_POST as $key => $value) {
        // strip whitespace from $value if not an array
        if (!is_array($value)) {
            $value = trim($value);
        }
        if (!in_array($key, $expected)) {
            // ignore the value, it's not in $expected
            continue;
        }
        if (in_array($key, $required) && empty($value)) {
            // required value is missing
            $missing[] = $key;
            $$key = "";
            continue;
        }
        $$key = $value;
    }

    This foreach loop processes the $_POST array by stripping out leading and trailing whitespace from text fields and assigning the field’s contents to a variable with a simplified name. As a result, $_POST['email'] becomes $email, and so on. It also checks if required fields are left blank, and adds them to the $missing array, setting the related variable to an empty string.

    The $_POST array is an associative array, so the loop assigns the key and value of the current element to $key and $value, respectively. The loop begins by checking that the current value isn’t an array, using the is_array() function with the logical Not operator (!). If it isn’t, the trim() function strips leading and trailing whitespace and reassigns it to $value. Removing leading and trailing whitespace prevents anyone from pressing the space bar several times to avoid filling in a required field.

     

Note

The form currently has only text input fields, but it will be expanded later to include <select>  and check box elements that submit data as arrays. It’s necessary to check whether the value of the current element is an array because passing an array to the trim() function triggers an error.

The next conditional statement checks whether the current key is not in the $expected array. If it isn’t, the continue keyword forces the loop to stop processing the current element and move onto the next one. So, anything not in the $expected array is ignored.

Next, we check if the current array key is in the $required array and if it has no value. If the condition returns true, the key is added to the $missing array and a variable based on the key’s name is created dynamically and its value is set to an empty string. Notice that $$key begins with two dollar signs in the following line:
$$key = "";

This means it’s a variable variable (see “Creating new variables dynamically” in Chapter 4). So, if the value of $key is “name,” $$key becomes $name.

Again, continue moves the loop onto the next element.

But, if we get all the way to the final line of the loop, we know that we’re dealing with an element that needs to be processed, so a variable based on the key name is created dynamically and the current value is assigned to it.
  1. 7.

    Save processmail.php. You’ll add more code to it later, but let’s turn now to the main body of contact.php. The action attribute in the opening form tag is empty. For local testing purposes, just set its value to the name of the current page:

    <form method="post" action="contact.php">
     
  2. 8.
    You need to display a warning if anything is missing. Add a conditional statement at the top of the page content between the <h2> heading and the first paragraph, like this:
    <h2>Contact us</h2>
    <?php if ($missing || $errors) { ?>
    <p class="warning">Please fix the item(s) indicated.</p>
    <?php } ?>
    <p>Ut enim ad minim veniam . . . </p>

    This checks $missing and $errors, which you initialized as empty arrays in step 1. As explained in “The truth according to PHP” in Chapter 4, an empty array is treated as false, so the paragraph inside the conditional statement isn’t displayed when the page first loads. However, if a required field hasn’t been filled in when the form is submitted, its name is added to the $missing array. An array with at least one element is treated as true. The || means “or,” so this warning paragraph will be displayed if a required field is left blank or if an error is discovered. (The $errors array comes into play in PHP Solution 6-4.)

     
  3. 9.

    To make sure it works so far, save contact.php and load it normally in a browser (don’t click the Refresh button). The warning message is not displayed. Click Send message without filling in any of the fields. You should now see the message about missing items, as shown in the following screenshot.

     
../images/332054_4_En_6_Chapter/332054_4_En_6_Fige_HTML.jpg
  1. 10.
    To display a suitable message alongside each missing required field, use a PHP conditional statement to insert a <span> inside the <label> tag, like this:
    <label for="name">Name:
    <?php if (in_array('name', $missing)) { ?>
        <span class="warning">Please enter your name</span>
    <?php } ?>
    </label>

    The condition uses the in_array() function to check if the $missing array contains the value name. If it does, the <span> is displayed. $missing is defined as an empty array at the top of the script, so the span won’t be displayed when the page first loads.

     
  2. 11.
    Insert similar warnings for the email and comments fields like this:
        <label for="email">Email:
        <?php if (in_array('email', $missing)) { ?>
            <span class="warning">Please enter your email address</span>
        <?php } ?>
        </label>
        <input name="email" id="email" type="text">
    </p>
    <p>
        <label for="comments">Comments:
        <?php if (in_array('comments', $missing)) { ?>
            <span class="warning">Please enter your comments</span>
        <?php } ?>
        </label>

    The PHP code is the same except for the value you are looking for in the $missing array. It’s the same as the name attribute for the form element.

     
  3. 12.

    Save contact.php and test the page again, first by entering nothing into any of the fields. The form labels should look like Figure 6-5.

     
../images/332054_4_En_6_Chapter/332054_4_En_6_Fig5_HTML.jpg
Figure 6-5.

By validating user input, you can display warnings about required fields

Although you added a warning to the <label> for the email field, it’s not displayed because email hasn’t been added to the $required array. As a result, it’s not added to the $missing array by the code in processmail.php.
  1. 13.

    Add email to the $required array in the code block at the top of comments.php, like this:

    $required = ['name', 'comments', 'email'];
     
  2. 14.

    Click Send message again without filling in any fields. This time, you’ll see a warning message alongside each label.

     
  3. 15.

    Type your name in the Name field. In the Email and Comments fields, just press the spacebar several times, then click Send message. The warning message alongside the Name field disappears, but the other two warning messages remain. The code in processmail.php strips whitespace from text fields, so it rejects attempts to bypass required fields by entering a series of spaces.

    If you have any problems, compare your code with contact_03.php and includes/processmail_01.php in the ch06 folder.

     

All that needs to be done to change the required fields is to change the names in the $required array and add a suitable alert inside the <label> tag of the appropriate input element inside the form. It’s easy to do because you always use the name attribute of the form input element.

Preserving user input when a form is incomplete

Imagine you have spent 10 minutes filling in a form. You click the Submit button, and back comes the response that a required field is missing. It’s infuriating if you have to fill in every field all over again. Since the content of each field is in the $_POST array, it’s easy to redisplay it when an error occurs.

PHP Solution 6-3: Creating Sticky Form Fields

This PHP solution shows how to use a conditional statement to extract the user’s input from the $_POST array and redisplay it in text input fields and text areas.

Continue working with the same files as before. Alternatively, use contact_03.php and includes/processmail_01.php from the ch06 folder.
  1. 1.
    When the page first loads, you don’t want anything to appear in the input fields, but you do want to redisplay the content if a required field is missing or there’s an error. That’s the key: if the $missing or $errors arrays contain any values, the content of each field should be redisplayed. You set default text for a text input field with the value attribute of the <input> tag, so amend the <input> tag for name like this:
    <input name="name" id="name" type="text"
    <?php if ($missing || $errors) {
        echo 'value="' . htmlentities($name) . '"';
    } ?>>
    The line inside the curly braces contains a combination of quotes and periods that might confuse you. The first thing to realize is that there’s only one semicolon—right at the end—so the echo command applies to the whole line. As explained in Chapter 3, a period is called the concatenation operator, which joins strings and variables. You can break down the rest of the line into three sections, as follows:
    • 'value="' .

    • htmlentities($name)

    • . '"'

    The first section outputs value=" as text and uses the concatenation operator to join it to the next section, which passes $name to a function called htmlentities(). I’ll explain why that’s necessary in a moment, but the third section uses the concatenation operator again to join the final output, which consists solely of a double quote. So, if $missing or $errors contain any values, and $_POST['name'] contains Joe, you’ll end up with this inside the <input> tag:
    <input name="name" id="name" type="text" value="Joe">

    The $name variable contains the original user input, which was transmitted through the $_POST array. The foreach loop that you created in processmail.php in PHP Solution 6-2 processes the $_POST array and assigns each element to a variable with the same name. This allows you to access $_POST['name'] simply as $name.

    So, why do we need the htmlentities() function? As the function name suggests, it converts certain characters to their equivalent HTML character entities. The one you’re concerned with here is the double quote. Let’s say Eric “Slowhand” Clapton decides to send feedback through the form. If you use $name on its own, Figure 6-6 shows what happens when a required field is omitted and you don’t use htmlentities().
    ../images/332054_4_En_6_Chapter/332054_4_En_6_Fig6_HTML.jpg
    Figure 6-6.

    Quotes need special treatment before form fields can be redisplayed

    Passing the content of the $_POST array element to the htmlentities(), however, converts the double quotes in the middle of the string to &quot;. And, as Figure 6-7 shows, the content is no longer truncated.
    ../images/332054_4_En_6_Chapter/332054_4_En_6_Fig7_HTML.jpg
    Figure 6-7.

    Passing the value to htmlentities() before it’s displayed solves the problem

    What’s cool about this is that the character entity &quot; is converted back to double quotes when the form is resubmitted. As a result, there’s no need for further conversion before the email can be sent.

     

Note

If htmlentities() corrupts your text, you can set the encoding directly within a script by passing the second and third optional arguments to the function. For example, to set the encoding to Simplified Chinese, use htmlentities($name, ENT_COMPAT, 'GB2312'). For details, see the documentation at www.php.net/manual/en/function.htmlentities.php .

  1. 2.

    Edit the email field the same way, using $email instead of $name.

     
  2. 3.
    The comments text area needs to be handled slightly differently because <textarea> tags don’t have a value attribute. You must place the PHP block between the opening and closing tags of the text area, like this (new code is shown in bold):
    <textarea name="comments" id="comments"><?php
      if ($missing || $errors) {
          echo htmlentities($comments);
      } ?></textarea>

    It’s important to position the opening and closing PHP tags right up against the <textarea> tags. If you don’t, you’ll get unwanted whitespace inside the text area.

     
  3. 4.

    Save contact.php and test the page in a browser. If any required fields are omitted, the form displays the original content along with any error messages.

    You can check your code with contact_04.php in the ch06 folder.

     

Caution

Using this technique prevents a form’s reset button from resetting any fields that have been changed by the PHP script because it explicitly sets the value attribute of each field.

Filtering Out Potential Attacks

A particularly nasty exploit known as email header injection seeks to turn online forms into spam relays. The attacker tries to trick your script into sending HTML email with copies to many people. This is possible if you incorporate unfiltered user input in the additional headers that can be passed as the fourth argument to the mail() function. It’s common to add the user’s email address as a Reply-to header. If you detect spaces, new lines, carriage returns, or any of the strings “Content-type:”, “Cc:”, or “Bcc:” in the submitted value, you’re the target of an attack, so you should block the message.

PHP Solution 6-4: Blocking email addresses that Contain Suspect Content

This PHP solution checks the user’s email address input for suspect content. If it’s detected, a Boolean variable is set to true. This will be used later to prevent the email from being sent.

Continue working with the same page as before. Alternatively, use contact_04.php and includes/processmail_01.php from the ch06 folder.
  1. 1.
    To detect the suspect phrases, we’ll use a search pattern or regular expression. Add the following code at the top of processmail.php before the existing foreach loop:
    // pattern to locate suspect phrases
    $pattern = '/[s ]|Content-Type:|Bcc:|Cc:/i';
    foreach ($_POST as $key => $value) {

    The string assigned to $pattern will be used to perform a case-insensitive search for any of the following: a space, carriage return, newline feed, “Content-Type:”, “Bcc:”, or “Cc:”. It’s written in a format called Perl-compatible regular expression (PCRE ). The search pattern is enclosed in a pair of forward slashes, and the i after the final slash makes the pattern case-insensitive.

     

Tip

Regular expressions are an extremely powerful tool for matching text patterns. Admittedly, they’re not easy to learn; but it’s an essential skill if you’re serious about working with programming languages such as PHP and JavaScript. Take a look at Introducing Regular Expressions by Jörg Krause (Apress, 2017, ISBN 978-1-4842-2508-0). It’s aimed primarily at JavaScript developers, but there are only minor differences of implementation between JavaScript and PHP. The basic syntax is identical.

  1. 2.
    You can now use the PCRE stored in $pattern to detect any suspect user input in the submitted email address. Add the following code immediately after the $pattern variable from step 1:
    // check the submitted email address
    $suspect = preg_match($pattern,  $_POST['email']);

    The preg_match() function compares the regular expression passed as the first argument against the value in the second argument, in this case, the value from the email field. It returns true if it finds a match. So, if any of the suspect content is found, $suspect will be true. But if there’s no match, it will be false.

     
  2. 3.
    If suspect content is detected in the email address, there’s no point in processing the $_POST array any further. Wrap the code that processes the $_POST variables in a conditional statement like this:
    if (!$suspect) {
        foreach ($_POST as $key => $value) {
            // strip whitespace from $value if not an array
            if (!is_array($value)) {
               $value = trim($value);
            }
            if (!in_array($key, $expected)) {
                // ignore the value, it's not in $expected
                continue;
            }
            if (in_array($key, $required) && empty($value)) {
                // required value is missing
                $missing[] = $key;
                $$key = "";
                continue;
            }
        $$key = $value;
        }
    }

    This processes the variables in the $_POST array only if $suspect is not true.

    Don’t forget the extra curly brace to close the conditional statement.

     
  3. 4.
    Edit the PHP block after the <h2> heading in contact.php to add a new warning message above the form, like this:
    <h2>Contact Us</h2>
    <?php if ($_POST && $suspect) { ?>
        <p class="warning">Sorry, your mail could not be sent.
        Please try later.</p>
    <?php } elseif ($missing || $errors) { ?>
      <p class="warning">Please fix the item(s) indicated.</p>
    <?php } ?>

    This sets a new condition that takes priority over the original warning message by being considered first. It checks if the $_POST array contains any elements—in other words, the form has been submitted—and if $suspect is true. The warning is deliberately neutral in tone. There’s no point in provoking attackers.

     
  4. 5.

    Save contact.php and test the form by typing any of the suspect content in the email field. You should see the new warning message, but your input won’t be preserved.

    You can check your code against contact_05.php and includes/processmail_02.php in the ch06 folder.

     

Sending Email

Before proceeding any further, it’s necessary to explain how the PHP mail() function works, because it will help you understand the rest of the processing script.

The PHP mail() function takes up to five arguments, all of them strings, as follows:
  • The address(es) of the recipient(s)

  • The subject line

  • The message body

  • A list of other email headers (optional)

  • Additional parameters (optional)

Email addresses in the first argument can be in either of the following formats:
To send to more than one address, use a comma-separated string like this:

The message body must be presented as a single string. This means that you need to extract the input data from the $_POST array and format the message, adding labels to identify each field. By default, the mail() function supports only plain text. New lines must use both a carriage return and newline character. It’s also recommended to restrict the length of lines to no more than 78 characters. Although it sounds complicated, you can build the message body automatically with about 20 lines of PHP code, as you’ll see in PHP Solution 6-6. Adding other email headers is covered in detail in the next section.

Many hosting companies now make the fifth argument a requirement. It ensures that the email is sent by a trusted user, and it normally consists of your own email address prefixed by -f (without a space in between), all enclosed in quotes. Check your hosting company’s instructions to see whether this is required and the exact format it should take.

Caution

You should never incorporate user input into the fifth argument to the mail() function because it can be used to execute arbitrary script on the web server.

Using additional email Headers Safely

You can find a full list of email headers at www.faqs.org/rfcs/rfc2076 , but some of the most well-known and useful ones enable you to send copies of an email to other addresses (Cc and Bcc) or to change the encoding. Each new header, except the final one, must be on a separate line terminated by a carriage return and newline character. This means using the and escape sequences in double-quoted strings (see Table 4-5 in Chapter 4).

Tip

A convenient way to format additional headers is to define each header as a separate array element and then use the implode() function to join them together with " " in double quotes.

By default, mail() uses Latin1 (ISO-8859-1) encoding, which doesn’t support accented characters. Web page editors these days frequently use Unicode (UTF-8), which supports most written languages, including the accents commonly used in European languages, as well as nonalphabetic scripts, such as Chinese and Japanese. To ensure that email messages aren’t garbled, use the Content-Type header to set the encoding to UTF-8, like this:
$headers[] = "Content-Type: text/plain; charset=utf-8";
You also need to add UTF-8 as the charset attribute in a <meta> tag in the <head> of your web pages like this:
<meta charset="utf-8">
Let’s say you want to send copies to other departments, plus a copy to another address that you don’t want the others to see. Email sent by mail() is often identified as coming from nobody@yourdomain (or whatever username is assigned to the web server), so it’s a good idea to add a more user-friendly “From” address. This is how you build those additional headers, finally joining them with implode():
$headers[] = 'From: Japan Journey<[email protected]>';
$headers[] = 'Bcc: [email protected]';
$headers = implode(" ", $headers);

The implode() function converts an array into a string by joining each array element with the string supplied as the first argument. So, this returns the $headers array as a single string with a carriage return and newline character between each array element.

After building the set of headers you want to use, you pass the variable containing them as the fourth argument to mail(), like this (assuming that the destination address, subject, and message body have already been stored in variables):
$mailSent = mail($to, $subject, $message, $headers);

Hard-coded additional headers like this present no security risk, but anything that comes from user input must be filtered before it’s used. The biggest danger comes from a text field that asks for the user’s email address. A widely used technique is to incorporate the user’s email address into a From or Reply-To header, which enables you to reply directly to incoming messages by clicking the Reply button in your email program. It’s very convenient, but attackers frequently try to pack an email input field with a large number of spurious headers. The previous PHP solution eliminated the headers most commonly used by attackers, but we need to check the email address further before incorporating it into the additional headers.

Caution

Although email fields are the prime target for attackers, the destination address and subject line are both vulnerable if you let users change the value. User input should always be regarded as suspect. Always hard-code the destination address and subject line. Alternatively, provide a drop-down menu of acceptable values and check the submitted value against an array of the same values.

PHP Solution 6-5: Adding headers and automating the reply address

This PHP solution adds three headers to the email: From, Content-Type (to set the encoding to UTF-8), and Reply-To. Before adding the user’s email address to the final header, it uses a built-in PHP filter to verify that the submitted value conforms to the format of a valid email address.

Continue working with the same page as before. Alternatively, use contact_05.php and includes/processmail_02.php from the ch06 folder.
  1. 1.
    Headers are often specific to a particular web site or page, so the From and Content-Type headers will be added to the script in contact.php. Add the following code to the PHP block at the top of the page just before processmail.php is included:
    $required = ['name', 'comments', 'email'];
    // create additional headers
    $headers[] = 'From: Japan Journey<[email protected]>';
    $headers[] = 'Content-Type: text/plain; charset=utf-8';
    require './includes/processmail.php';
     
  2. 2.

    The purpose of validating the email address is to make sure it’s in a valid format, but the field might be empty because you decide not to make it required or because the user simply ignored it. If the field is required but empty, it will be added to the $missing array, and the warning you added in PHP Solution 6-2 will be displayed. If the field isn’t empty, but the input is invalid, you need to display a different message.

    Switch to processmail.php and add this code at the bottom of the script:
    // validate the user's email
    if (!$suspect && !empty($email)) {
        $validemail = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
        if ($validemail) {
            $headers[] = "Reply-To: $validemail";
        } else {
            $errors['email'] = true;
        }
    }

    This begins by checking that no suspect content has been found and that the email field isn’t empty. Both conditions are preceded by the logical Not operator, so they return true if $suspect and empty($email) are both false. The foreach loop you added in PHP Solution 6-2 assigns all expected elements in the $_POST array to simpler variables, so $email contains the same value as $_POST['email'].

    The next line uses filter_input() to validate the email address. The first argument is a PHP constant, INPUT_POST, which specifies that the value must be in the $_POST array. The second argument is the name of the element you want to test. The final argument is another PHP constant that specifies you want to check that the element conforms to the valid format for an email.

    The filter_input() function returns the value being tested if it’s valid. Otherwise, it returns false. So, if the value submitted by the user looks like a valid email address, $validemail contains the address. If the format is invalid, $validemail is false. The FILTER_VALIDATE_EMAIL constant accepts only a single email address, so any attempt to insert multiple email addresses will be rejected.

     

Note

FILTER_VALIDATE_EMAIL checks the format, not whether the address is genuine.

If $validemail isn’t false, it’s safe to incorporate into a Reply-To email header. But if $validemail is false, $errors['email'] is added to the $errors array.
  1. 3.
    You now need to amend the <label> for the email field in contact.php, like this:
    <label for="email">Email:
    <?php if (in_array('email', $missing)) { ?>
        <span class="warning">Please enter your email address</span>
    <?php } elseif (isset($errors['email'])) { ?>
        <span class="warning">Invalid email address</span>
    <?php } ?>
    </label>

    This adds an elseif clause to the first conditional statement and displays a different warning if the email address fails validation.

     
  2. 4.

    Save contact.php and test the form by leaving all fields blank and clicking Send message. You’ll see the original error message. Test it again by entering a value that isn’t an email address in the Email field or by entering two email addresses. You should see the invalid message.

    You can check your code with contact_06.php and includes/processmail_03.php in the ch06 folder.

     

PHP Solution 6-6: Building the message body and sending the mail

Many PHP tutorials show how to build the message body manually like this:
$message = "Name: $name ";
$message .= "Email: $email ";
$message .= "Comments: $comments";

This adds labels to identify which field the input comes from and inserts two carriage returns and newline characters between each one. This is fine for a small number of fields, but it soon becomes tedious with more fields. As long as you give your form fields meaningful name attributes, you can build the message body automatically with a foreach loop, which is the approach taken in this PHP solution.

Continue working with the same files as before. Alternatively, use contact_06.php and includes/processmail_03.php from the ch06 folder.
  1. 1.

    Add the following code at the bottom of the script in processmail.php:

    $mailSent = false;

    This initializes a variable to redirect to a thank you page after the mail has been sent. It needs to be set to false until you know the mail() function has succeeded.

     
  2. 2.
    Now add the code that builds the message immediately after:
    // go ahead only if not suspect, all required fields OK, and no errors
    if (!$suspect && !$missing && !$errors) {
        // initialize the $message variable
        $message = ";
        // loop through the $expected array
        foreach($expected as $item) {
            // assign the value of the current item to $val
            if (isset($$item) && !empty($$item)) {
                $val = $$item;
            } else {
                // if it has no value, assign 'Not selected'
                $val = 'Not selected';
            }
            // if an array, expand as comma-separated string
            if (is_array($val)) {
                $val = implode(', ', $val);
            }
            // replace underscores in the label with spaces
            $item = str_replace('_', ' ', $item);
            // add label and value to the message body
            $message .= ucfirst($item).": $val ";
        }
        // limit line length to 70 characters
        $message = wordwrap($message, 70);
        // format headers as a single string
        $headers = implode(" ", $headers);
        $mailSent = true;
    }

    This block of code begins by checking that $suspect, $missing, and $errors are all false. If they are, it builds the message body by looping through the $expected array, storing the result in $message as a series of label/value pairs.

    The key to how it works lies in the following conditional statement:
    if (isset($$item) && !empty($$item)) {
        $val = $$item;
    }
    This is another example of using a variable variable (see “Creating new variables dynamically” in Chapter 4). Each time the loop runs, $item contains the value of the current element in the $expected array. The first element is name, so $$item dynamically creates a variable called $name. In effect, the conditional statement becomes this:
    if (isset($name) && !empty($name)) {
        $val = $name;
    }

    On the next pass, $$item creates a variable called $email, and so on.

     

Caution

This script builds the message body only from items in the $expected array. You must list the names of all form fields in the $expected array for it to work.

If a field not specified as required is left empty, its value is set to “Not selected.” The code also processes values from multiple-choice elements, such as check-box groups and <select> lists, which are transmitted as subarrays of the $_POST array. The implode() function converts the subarrays into comma-separated strings.

Each label is derived from the input field’s name attribute in the current element of the $expected array. The first argument to str_replace() is an underscore. If an underscore is found in the name attribute, it’s replaced by the second argument, a string consisting of a single space. The first letter is then set to uppercase by ucfirst(). Notice that the third argument to str_replace() is $item (with a single dollar sign), so this time it’s an ordinary variable, not a variable variable. It contains the current value from the $expected array.

After the message body has been combined into a single string, the wordwrap() function limits the line length to 70 characters. The headers are formatted as a single string with a carriage return and newline character between each one using the implode() function .

The code that sends the email still needs to be added, but for testing purposes, $mailSent is set to true.
  1. 3.
    Save processmail.php. Locate this code block at the bottom of contact.php:
    <pre>
    <?php if ($_POST) {print_r($_POST);} ?>
    </pre>
    Change it to this:
    <pre>
    <?php if ($_POST && $mailSent) {
        echo "Message body ";
        echo htmlentities($message) . " ";
        echo 'Headers: '. htmlentities($headers);
    } ?>
    </pre>

    This checks that the form has been submitted and the mail is ready to send. It then displays the values in $message and $headers. Both values are passed to htmlentities() to ensure they display correctly in the browser.

     
  2. 4.
    Save contact.php, and test the form by entering your name, email address, and a brief comment. When you click Send message, you should see the message body and headers displayed at the bottom of the page, as shown in Figure 6-8.
    ../images/332054_4_En_6_Chapter/332054_4_En_6_Fig8_HTML.jpg
    Figure 6-8.

    Verifying that the message body and headers are correctly formed

    Assuming the message body and headers display correctly at the bottom of the page, you’re ready to add the code to send the email. If necessary, check your code against contact_07.php and includes/processmail_04.php in the ch06 folder.

     
  3. 5.

    In processmail.php, add the code to send the mail. Locate the following line:

    $mailSent = true;
    Change it to this:
    $mailSent = mail($to, $subject, $message, $headers);
    if (!$mailSent) {
        $errors['mailfail'] = true;
    }

    This passes the destination address, subject line, message body, and headers to the mail() function, which returns true if it succeeds in handing the email to the web server’s mail transport agent (MTA). If it fails, $mailSent is set to false, and the conditional statement adds an element to the $errors array, allowing you to preserve the user’s input when the form is redisplayed.

     
  4. 6.
    In the PHP block at the top of contact.php, add the following conditional statement immediately after the command that includes processmail.php:
        require './includes/processmail.php';
        if ($mailSent) {
            header('Location: http://www.example.com/thank_you.php');
            exit;
        }
    }
    ?>

    You need to test this on your remote server, so replace www.example.com with your own domain name. This checks if $mailSent is true. If it is, the header() function redirects to thank_you.php, a page acknowledging the message has been sent. The exit command on the following line ensures the script is terminated after the page has been redirected.

    There’s a copy of thank_you.php in the ch06 folder.

     
  5. 7.
    If $mailSent is false, contact.php is redisplayed; you need to warn the user that the message couldn’t be sent. Edit the conditional statement just after the <h2> heading, like this:
    <h2>Contact Us </h2>
    <?php if (($_POST && $suspect) || ($_POST && isset($errors['mailfail']))) { ?>
        <p class="warning">Sorry, your mail could not be sent. . . .

    The original and new conditions have been wrapped in parentheses, so each pair is considered separately. The warning about the message not being sent is displayed if the form has been submitted and suspect phrases have been found, or if the form has been submitted and $errors['mailfail'] has been set.

     
  6. 8.

    Delete the code block (including the <pre> tags) that displays the message body and headers at the bottom of contact.php.

     
  7. 9.

    Testing this locally is likely to result in the thank you page being shown, but the email never arriving. This is because most testing environments don’t have an MTA. Even if you set one up, most mail servers reject mail from unrecognized sources. Upload contact.php and all related files, including processmail.php and thank_you.php, to your remote server and test the contact form there. Don’t forget that processmail.php needs to be in a subfolder called includes.

    You can check your code with contact_08.php and includes/processmail_05.php in the ch06 folder.

     

Troubleshooting mail()

It’s important to understand that mail() isn’t an email program. PHP’s responsibility ends as soon as it passes the address, subject, message, and headers to the MTA. It has no way of knowing if the email is delivered to its intended destination. Normally, email arrives instantaneously, but network logjams can delay it by hours or even a couple of days.

If you’re redirected to the thank you page after sending a message from contact.php, but nothing arrives in your inbox, check the following:
  • Has the message been caught by a spam filter?

  • Have you checked the destination address stored in $to? Try an alternative email address to see if it makes a difference.

  • Have you used a genuine address in the From header? Using a fake or invalid address is likely to cause the mail to be rejected. Use a valid address that belongs to the same domain as your web server.

  • Check with your hosting company to see if the fifth argument to mail() is required. If so, it should normally be a string composed of -f followed by your email address. For example, [email protected] becomes '-[email protected]'.

If you still don’t receive messages from contact.php, create a file with this script:
<?php
ini_set('display_errors', '1');
$mailSent = mail('[email protected]', 'PHP mail test', 'This is a test email');
if ($mailSent) {
    echo 'Mail sent';
} else {
    echo 'Failed';
}

Replace [email protected] with your own email address. Upload the file to your web site and load the page into a browser.

If you see an error message about there being no From header, add one as a fourth argument to the mail() function, like this:
$mailSent = mail('[email protected]', 'PHP mail test', 'This is a test email',

It’s usually a good idea to use a different address from the destination address in the first argument.

If your hosting company requires the fifth argument, adjust the code like this:
$mailSent = mail('[email protected]', 'PHP mail test', 'This is a test email', null,

Using the fifth argument normally replaces the need to supply a From header, so using null (without quotes) as the fourth argument indicates that it has no value.

If you see “Mail sent” and no mail arrives, or you see “Failed” after trying all five arguments, consult your hosting company for advice.

If you receive the test email from this script but not from contact.php, it means you have made a mistake in the code or that you have forgotten to upload processmail.php. Turn on the display of errors temporarily, as described in “Why is my page blank?” in Chapter 3, to check contact.php is able to find processmail.php.

Tip

I was teaching at a university in England and couldn’t work out why students’ mails weren’t being delivered, even though their code was perfect. It turned out the IT department had disabled Sendmail (the MTA) to prevent the server being used to deliver spam!

Handling Multiple-Choice Form Elements

The form in contact.php uses only text input fields and a text area. To work successfully with forms, you also need to know how to handle multiple-choice elements, namely:
  • Radio buttons

  • Check boxes

  • Drop-down option menus

  • Multiple-choice lists

The principle behind them is the same as the text input fields you have been working with: the name attribute of the form element is used as the key in the $_POST array. However, there are some important differences:
  • Check-box groups and multiple-choice lists store selected values as an array, so you need to add an empty pair of square brackets at the end of the name attribute for these types of input. For example, for a check-box group called interests, the name attribute in each <input> tag should be name="interests[]". If you omit the square brackets, only the last item selected is transmitted through the $_POST array.

  • The values of selected items in a check-box group or multiple-choice list are transmitted as a subarray of the $_POST array. The code in PHP Solution 6-6 automatically converts these subarrays to comma-separated strings. However, when using a form for other purposes, you need to extract the values from the subarrays. You’ll see how to do so in later chapters.

  • Radio buttons, check boxes, and multiple-choice lists are not included in the $_POST array if no value is selected. Consequently, it’s vital to use isset() to check for their existence before attempting to access their values when processing the form.

The remaining PHP solutions in this chapter show how to handle multiple-choice form elements. Rather than go through each step in detail, I’ll just highlight the important points. Bear the following points in mind when working through the rest of this chapter:
  • Processing these elements relies on the code in processmail.php.

  • You must add the name attribute of each element to the $expected array for it to be added to the message body.

  • To make a field required, add its name attribute to the $required array.

  • If a field that’s not required is left blank, the code in processmail.php sets its value to “Not selected.”

Figure 6-9 shows contact.php with each type of input added to the original design.
../images/332054_4_En_6_Chapter/332054_4_En_6_Fig9_HTML.jpg
Figure 6-9.

The feedback form with examples of multiple-choice form elements

Tip

HTML5 form input elements all use the name attribute and send values as text or as a subarray of the $_POST array, so you should be able to adapt the code accordingly.

PHP Solution 6-7: Handling Radio Button Groups

Radio button groups let you pick only one value. Although it’s common to set a default value in the HTML markup, it’s not obligatory. This PHP solution shows how to handle both scenarios.
  1. 1.

    The simple way to deal with radio buttons is to make one of them the default. The radio group is always included in the $_POST array because a value is always selected.

    The code for a radio group with a default value looks like this (the name attributes and PHP code are highlighted in bold):
    <fieldset id="subscribe">
        <h2>Subscribe to newsletter?</h2>
        <p>
        <input name="subscribe" type="radio" value="Yes" id="subscribe-yes"
        <?php
        if ($_POST && $_POST['subscribe'] == 'Yes') {
            echo 'checked';
        } ?>>
        <label for="subscribe-yes">Yes</label>
        <input name="subscribe" type="radio" value="No" id="subscribe-no"
        <?php
        if (!$_POST || $_POST['subscribe'] == 'No') {
           echo 'checked';
        } ?>>
        <label for="subscribe-no">No</label>
        </p>
    </fieldset>

    All members of the radio group share the same name attribute. Because only one value can be selected, the name attribute does not end with a pair of empty brackets.

    The conditional statement related to the Yes button checks $_POST to see if the form has been submitted. If it has and the value of $_POST['subscribe'] is “Yes,” the checked attribute is added to the <input> tag.

    In the No button, the conditional statement uses || (or). The first condition is !$_POST, which is true when the form hasn’t been submitted. If true, the checked attribute is added as the default value when the page first loads. If false, it means the form has been submitted, so the value of $_POST['subscribe'] is checked.

     
  2. 2.
    When a radio button doesn’t have a default value, it’s not included in the $_POST array, so it isn’t detected by the loop in processmail.php that builds the $missing array. To ensure that the radio button element is included in the $_POST array, you need to test for its existence after the form has been submitted. If it isn’t included, you need to set its value to an empty string, like this:
    $required = ['name', 'comments', 'email', 'subscribe'];
    // set default values for variables that might not exist
    if (!isset($_POST['subscribe'])) {
        $_POST['subscribe'] = ";
    }
     
  3. 3.

    If the radio button group is required but not selected, you need to display an error message when the form reloads. You also need to change the conditional statements in the <input> tags to reflect the different behavior.

    The following listing shows the subscribe radio button group from contact_09.php, with all the PHP code highlighted in bold:
    <fieldset id="subscribe">
        <h2>Subscribe to newsletter?
        <?php if (in_array('subscribe', $missing)) { ?>
        <span class="warning">Please make a selection</span>
        <?php } ?>
        </h2>
        <p>
        <input name="subscribe" type="radio" value="Yes" id="subscribe-yes"
        <?php
        if ($_POST && $_POST['subscribe'] == 'Yes') {
            echo 'checked';
        } ?>>
        <label for="subscribe-yes">Yes</label>
        <input name="subscribe" type="radio" value="No" id="subscribe-no"
        <?php
        if ($_POST && $_POST['subscribe'] == 'No') {
            echo 'checked';
        } ?>>
        <label for="subscribe-no">No</label>
        </p>
    </fieldset>

    The conditional statement that controls the warning message in the <h2> tag uses the same technique as for the text input fields. The message is displayed if the radio group is a required item and it’s in the $missing array.

    The conditional statement surrounding the checked attribute is the same in both radio buttons. It checks if the form has been submitted and displays the checked attribute only if the value in $_POST['subscribe'] matches.

     

PHP Solution 6-8: Handling check-box groups

Check boxes can be used individually or in groups. The method of handling them is slightly different. This PHP solution shows how to deal with a check-box group called interests. PHP Solution 6-11 explains how to handle a single check box.

When used as a group, all check boxes in the group share the same name attribute, which needs to end with an empty pair of square brackets in order for PHP to transmit the selected values as an array. To identify which check boxes have been selected, each one needs a unique value attribute.

If no items are selected, the check-box group is not included in the $_POST array. After the form has been submitted, you need to check the $_POST array to see if it contains a subarray for the check-box group. If it doesn’t, you need to create an empty subarray as the default value for the script in processmail.php.
  1. 1.
    To save space, just the first two check boxes of the group are shown. The name attribute and PHP sections of code are highlighted in bold.
    <fieldset id="interests">
    <h2>Interests in Japan</h2>
    <div>
        <p>
            <input type="checkbox" name="interests[]" value="Anime/manga"
            id="anime"
            <?php
            if ($_POST && in_array('Anime/manga', $_POST['interests'])) {
                echo 'checked';
            } ?>>
            <label for="anime">Anime/manga</label>
        </p>
        <p>
            <input type="checkbox" name="interests[]" value="Arts & crafts"
            id="art"
            <?php
            if ($_POST && in_array('Arts & crafts', $_POST['interests'])) {
                echo 'checked';
            } ?>>
            <label for="art">Arts & crafts</label>
        </p>
    . . .
    </div>
    </fieldset>

    Each check box shares the same name attribute, which ends with an empty pair of square brackets, so the data is treated as an array. If you omit the brackets, $_POST['interests'] contains the value of only the first check box selected. Also, $_POST['interests'] won’t exist if no check box has been selected. You’ll fix that in the next step.

     

Note

Although the brackets must be added to the name attribute for multiple selections, the subarray of selected values is in $_POST['interests'], not $_POST['interests[]'].

The PHP code inside each check-box element performs the same role as in the radio button group, wrapping the checked attribute in a conditional statement. The first condition checks that the form has been submitted. The second condition uses the in_array() function to check whether the value associated with that check box is in the $_POST['interests'] subarray. If it is, it means the check box was selected.
  1. 2.
    After the form has been submitted, you need to check for the existence of $_POST['interests']. If it hasn’t been set, you must create an empty array as the default value for the rest of the script to process. The code follows the same pattern as for the radio group:
    $required = ['name', 'comments', 'email', 'subscribe', 'interests'];
    // set default values for variables that might not exist
    if (!isset($_POST['subscribe'])) {
        $_POST['subscribe'] = ";
    }
    if (!isset($_POST['interests'])) {
        $_POST['interests'] = [];
    }
     
  2. 3.
    To set a minimum number of required check boxes, use the count() function to confirm the number of values transmitted from the form. If it’s less than the minimum required, add the group to the $errors array, like this:
    if (!isset($_POST['interests'])) {
        $_POST['interests'] = [];
    }
    // minimum number of required check boxes
    $minCheckboxes = 2;
    if (count($_POST['interests']) < $minCheckboxes) {
        $errors['interests'] = true;
    }

    The count() function returns the number of elements in an array, so this creates $errors['interests'] if fewer than two check boxes have been selected. You might be wondering why I have used a variable instead of the number like this:

    if (count($_POST['interests']) < 2) {

    This certainly works and it involves less typing, but $minCheckboxes can be reused in the error message. Storing the number in a variable means this condition and the error message always remain in sync.

     
  3. 4.
    The error message in the body of the form looks like this:
    <h2>Interests in Japan
    <?php if (isset($errors['interests'])) { ?>
        <span class="warning">Please select at least <?= $minCheckboxes ?></span>
    <?php } ?>
    </h2>
     

PHP Solution 6-9: Using a drop-down Option Menu

Drop-down option menus created with the <select> tag are similar to radio button groups in that they normally allow the user to pick only one option from several. Where they differ is one item is always selected in a drop-down menu, even if it’s only the first item inviting the user to select one of the others. As a result, the $_POST array always contains an element referring to a <select> menu, whereas a radio button group is ignored unless a default value is preset.
  1. 1.
    The following code shows the first two items from the drop-down menu in contact_09.php, with the PHP code highlighted in bold. As with all multiple-choice elements, the PHP code wraps the attribute that indicates which item has been chosen. Although this attribute is called checked in both radio buttons and check boxes, it’s called selected in <select> menus and lists. It’s important to use the correct attribute to redisplay the selection if the form is submitted with required items missing. When the page first loads, the $_POST array contains no elements, so you can select the first <option> by testing for !$_POST. Once the form is submitted, the $_POST array always contains an element from a drop-down menu, so you don’t need to test for its existence.
    <p>
        <label for="howhear">How did you hear of Japan Journey?</label>
        <select name="howhear" id="howhear">
            <option value="No reply"
            <?php
            if (!$_POST || $_POST['howhear'] == 'No reply') {
                echo 'selected';
            } ?>>Select one</option>
            <option value="Apress"
            <?php
            if (isset($_POST && $_POST['howhear'] == 'Apress') {
                echo 'selected';
            } ?>>Apress</option>
        . . .
        </select>
    </p>
     
  2. 2.
    Even though an option is always selected in a drop-down menu, you might want to force users to make a selection other than the default. To do so, add the name attribute of the <select> menu to the $required array, then set the value attribute and the $_POST array element for the default option to an empty string, like this:
    <option value=""
    <?php
    if (!$_POST || $_POST['howhear'] == '') {
        echo 'selected';
    } ?>>Select one</option>

    The value attribute is not required in the <option> tag, but if you leave it out, the form uses the text between the opening and closing tags as the selected value. Therefore, it’s necessary to set the value attribute explicitly to an empty string. Otherwise, “Select one” is transmitted as the selected value.

     
  3. 3.
    The code that displays a warning message if no selection has been made follows a familiar pattern:
    <label for="select">How did you hear of Japan Journey?
    <?php if (in_array('howhear', $missing)) { ?>
        <span class="warning">Please make a selection</span>
    <?php } ?>
    </label>
     

PHP Solution 6-10: Handling a multiple-choice list

Multiple-choice lists are similar to check-box groups: they allow the user to choose zero or more items, so the result is stored in an array. If no items are selected, the multiple-choice list is not included in the $_POST array, so you need to add an empty subarray in the same way as with a check-box group.
  1. 1.
    The following code shows the first two items from the multiple-choice list in contact_09.php, with the name attribute and PHP code highlighted in bold. The square brackets appended to the name attribute ensure that it stores the results as an array. The code works in an identical way to the check-box group in PHP Solution 6-8.
    <p>
        <label for="characteristics">What characteristics do you associate with
        Japan?</label>
        <select name="characteristics[]" size="6" multiple="multiple"
        id="characteristics">
            <option value="Dynamic"
            <?php
            if ($_POST && in_array('Dynamic', $_POST['characteristics'])) {
                echo 'selected';
            } ?>>Dynamic</option>
            <option value="Honest"
            <?php
            if ($_POST && in_array('Honest', $_POST['characteristics'])) {
                echo 'selected';
            } ?>>Honest</option>
    . . .
        </select>
    </p>
     
  2. 2.
    In the code that processes the message, set a default value for a multiple-choice list in the same way as for an array of check boxes:
    if (!isset($_POST['interests'])) {
      $_POST['interests'] = [];
    }
    if (!isset($_POST['characteristics'])) {
      $_POST['characteristics'] = [];
    }
     
  3. 3.

    To make a multiple-choice list required and to set a minimum number of choices, use the same technique used for a check-box group in PHP Solution 6-8.

     

PHP Solution 6-11: Handling a Single Check Box

The way you handle a single check box is slightly different from a check-box group. With an individual check box, you don’t append square brackets to the name attribute because it doesn’t need to be processed as an array. Also, the value attribute is optional. If you don’t set the value attribute, it defaults to “On” if the check box is selected. However, if the check box isn’t selected, its name isn’t included in the $_POST array, so you need to test for its existence.

This PHP solution shows how to add a single check box that seeks confirmation that the site’s terms have been accepted. It assumes that selecting the check box is required.
  1. 1.
    This code shows the single check box, with the name attribute and PHP code highlighted in bold.
    <p>
        <input type="checkbox" name="terms" value="accepted" id="terms"
        <?php
        if ($_POST && !isset($errors['terms'])) {
            echo 'checked';
        } ?>>
        <label for="terms">I accept the terms of using this website
        <?php if (isset($errors['terms'])) { ?>
            <span class="warning">Please select the check box</span>
        <?php } ?></label>
    </p>

    The PHP block inside the <input> element inserts the checked attribute only if the $_POST array contains values and $errors['terms'] hasn’t been set. This ensures that the check box is not selected when the page first loads. It also remains unchecked if the user submitted the form without confirming acceptance of the terms.

    The second PHP block displays an error message alongside the label if $errors['terms'] has been set.

     
  2. 2.
    In addition to adding terms to the $expected and $required arrays, you need to set a default value for $_POST['terms']; then set $errors['terms'] in the code that processes the data when the form is submitted:
    if (!isset($_POST['characteristics'])) {
        $_POST['characteristics'] = [];
    }
    if (!isset($_POST['terms'])) {
        $_POST['terms'] = ";
        $errors['terms'] = true;
    }

    You need to create $errors['terms'] only if the check box is required. For an optional check box, just set the value to an empty string if it’s not included in the $_POST array.

     

Chapter Review

A lot of work has gone into building processmail.php, but the beauty of this script is that it works with any form. The only parts that need changing are the $expected and $required arrays and details specific to the form, such as the destination address, headers, and default values for multiple-choice elements that won’t be included in the $_POST array if no value is selected.

I’ve avoided talking about HTML email because the mail() function is poorly equipped to handle it. The PHP online manual at www.php.net/manual/en/book.mail.php shows a way of sending HTML mail by adding an additional header. However, it’s generally accepted that HTML mail should always contain an alternative text version for email programs that don’t accept HTML. If you want to send HTML mail or attachments, try PHPMailer ( https://github.com/PHPMailer/PHPMailer/ ).

As you’ll see in later chapters, online forms lie at the heart of just about everything you do with PHP. They’re the gateway between the browser and the web server. You’ll come back time and again to the techniques that you have learned in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.74.54