Saving form data to a file

Where are we?

You just learned how to send form data by email. But sometimes you want that data to be immediately available on your server. Let’s see one way to do that.

This lesson’s goals

By the end of this lesson, you should:

  • Know how to append form data to a file.
  • Be able to read back data from the file.
  • Know how to do some basic security stuff.

Comments – What the user sees

Let’s say you want people to be able to add comments to a Web page. Here’s what the user sees when there are no comments:

No comments

Figure 1. No comments

There are two pieces to the comment area:

Comment area pieces

Figure 2. Comment area pieces

First, there’s the current comments. There are none in this case.

Second, there’s a form for adding new comments. This will always be the same, no matter how many comments there are.

Users add comments by typing in the text field and clicking the button:

Adding a comment

Figure 3. Adding a comment

When they click Save, the page reloads, and shows the new comment in the comment area.

Comment showing

Figure 4. Comment showing

Where do comments go?

We need a place to keep comments that people enter, so we can read them back and show them on Renata’s page.

For this example, we’ll store them in a file. When there’s a new comment, we’ll append it to the file. Append means “add to the end.”

When Renata’s page – renata.php – loads, we’ll show the comments. We’ll put some PHP on the page that will read the comments in and insert them into the HTML.

This will work most easily if the comments file already contains HTML tags. But people who type comments don’t type HTML. They just type text:

Adding a comment

Figure 3 (again). Adding a comment

So we’ll add some HTML tags around what they type.

Whenever you save data that people type, you have to worry about security. You don’t want hackers typing comments that, for example, contain JavaScript. We’ll address that later.

Flow of control

Let’s draw a picture of what happens behind the scenes. It will show us the flow of control, that is, what happens first, what happens next, and so on.

Comment control flow

Figure 5. Comment control flow

The browser asks the server for renata.php (1). PHP code in renata.php reads the comments file, and inserts the comments into the HTML (2). renata.php also contains HTML to show the form (the <form> tag and friends).

Once the browser has shown the page, it waits for the user to do something. Suppose the user types a comment, and clicks the Save button (3). The browser looks at the action property of the form: action="save-renata-comment.php". So it asks the server for that page, and sends the form data along with the request (4).

The server loads save-renata-comment.php. Code in that file takes the new comment, wraps it in a <p> tag, and appends it to the comments file. Then it redirects the browser. That is, it tells the browser to load another page. Which one? renata.php (6).

So the browser loads renata.php (1). Code in renata.php reads the comments file (2), which now has the new comment on the end. So the new comment appears.

Phew! That’s a lot of work! But it’s a lot of work for the computers, not for us. We tell them what to do, and they do it.

Let’s have a look at the pieces.

Showing comments

Here’s what the comments file looks like after a comment has been added:

<p>You are so cute!</p><hr>

Figure 6. Comments file with one comment

We need to read that in to the right place in renata.php (2 in Figure 5). Here’s the code for the entire file. (I’ve wrapped it in the template code we saw earlier.)

<?php 
//Path from this page to the site root.
$path_to_root = '..';
//Title of this page.
$page_title = 'Renata';
require $path_to_root . '/library/start_page.inc';
require $path_to_root . '/library/header.inc';
require $path_to_root . '/library/left_nav.inc';
?>
<div id="center_region">
  <h1>Dog profile: Renata</h1>
  <p>Here is Renata.</p>
  <p><img src="renata.jpg" alt="Renata"></p> 
  <h2>Comments</h2>
  <?php
  if( file_exists('renata_comments.txt') ) {
    readfile('renata_comments.txt');
  }
  else {
    print '<p>There are no comments yet.</p>';
  }
  ?>
  <h2>Add a comment</h2>
  <form action="save-renata-comment.php" method="post">
    <p>
      <textarea rows="3" cols="30" name="comment"></textarea>
    </p>
    <p>
      <button type="submit">Save</button>
    </p>
  </form>
  <p><a href="index.php">Back to the dog list</a></p>
</div>
<?php
require $path_to_root . '/library/footer.inc';
require $path_to_root . '/library/end_page.inc';
?>

Figure 7. renata.php

Everything up to line 9 is templating code. The real stuff starts on line 10.

Lines 10 to 14 are simple HTML. That’s the main content of the page. The comment section follows.

There may be comments, or there may not be. If there are comments, we want to show them. Otherwise, we want to tell the user that there are no comments yet.

We’re going to store comments in the file renata_comments.txt. The file will be created when the first comment is added. If there are no comments, the file won’t even exist.

Here’s line 16:

if( file_exists('renata_comments.txt') ) {

file_exists() is a function that returns true if the file is found. If it is, line 17 runs:

readfile('renata_comments.txt');

This reads the file, and inserts it into the HTML output stream.

If the file does not exist, then file_exists('renata_comments.txt') is false, and line 20 runs:

print '<p>There are no comments yet.</p>';

This outputs the text in the quotes. Remember that we need to include the HTML tags.

The rest of the page uses stuff we’ve seen before.

Adding a new comment

The user types a comment and clicks the same button. The browser takes the URL in the form’s action attribute – save-renata-comment.php – and goes there, sending the contents of the comment field (4 in Figure 5).

Here’s save-renata-comment.php:

<?php
//Get the user's comment.
$comment = $_POST['comment'];
//Append it to the comments file.
$f = fopen('renata_comments.txt', 'a');
fwrite($f, "<p>$comment</p><hr>");
fclose($f);
//Jump back to Renata's page.
header('location:renata.php');
?>

Figure 8. save-renata-comment.php

Here’s what happens when the user clicks the Send button.

Browser
Browser
Web server
Web server
PHP interpreter
PHP interpreter
Gimme save-renata-comment.php.
And here's some form data:
comment=You+are+so+cute!
   
  (Fetches save-renata-comment.php from disk.)
Oh! This is a PHP file.
Hey, PHP interpreter! Run this.
 
    Get the form data.
Save data to the file.
Hey, server, here's the result. No HTML, but I have an HTTP header for you:
Location: renata.php
  Hey, browser. Here's the contents of save-renata-comment.php. Just an HTTP header:
Location: renata.php
 
OK.
Hey, send me renata.php.
   
  (Fetches renata.php from disk.)
Oh! This is a PHP file.
Hey, PHP interpreter! Run this.
 
    HTML, send it through.
Here's some PHP.
Read the file, put it in the HTML.
More HTML.
Hey, server, here's the result.
  Hey, browser. Here's the contents of renata.php.  
OK.
(Render for user.)
   

Figure 9. After the Save button is pressed

Let’s have a look at the code again.

<?php
//Get the user's comment.
$comment = $_POST['comment'];
//Append it to the comments file.
$f = fopen('renata_comments.txt', 'a');
fwrite($f, "<p>$comment</p><hr>");
fclose($f);
//Jump back to Renata's page.
header('location:renata.php');
?>

Figure 8 (again). save-renata-comment.php

Line 3 gets the user’s comment, and puts it into the variable $comment.

Line 5 is new. We want to write to the comments file. But before we do that, we need to open it. That’s what the fopen() function does.

We give fopen() two arguments:

  • The name of the file to open.
  • The mode to open the file in.

The 'a' means the file is opened in append mode. That means that anything we write to the file will be added to the end.

Take another look at the line:

$f = fopen('renata_comments.txt', 'a');

What is $f for? fopen() returns a file handle. This is a special object that refers to the file. You don’t need to know what it is, exactly. Just use it.

The next line is:

fwrite($f, "<p>$comment</p><hr>");

This writes data to $f. Because the program opened the file in append mode, the data is written to the end of the file.

Remember that it’s easier if the file contains HTML. But the user types just plain text into the comment field. So we wrap the comment in a <p> tag. We also add a horizontal rule (<hr>) to separate comments from each other.

Finally, the program closes the file in line 9. PHP will close files for you automatically if you forget to do it.

The page that isn’t a page

One more thing to notice about the code.

<?php
//Get the user's comment.
$comment = $_POST['comment'];
//Append it to the comments file.
$f = fopen('renata_comments.txt', 'a');
fwrite($f, "<p>$comment</p><hr>");
fclose($f);
//Jump back to Renata's page.
header('location:renata.php');
?>

Figure 8 (again). save-renata-comment.php

This program does not send any HTML to the browser. The program adds the new comment, then tells the browser to jump to another page.

PHP files don’t have to generate an <html> tag. Most do, but this one doesn’t. In future chapters, you’ll see other programs that have the same pattern:

  • Get some data from $_POST.
  • Do something with it.
  • Jump to another URL.

Is save-renata-comment.php really a Web “page?” Well, it’s a PHP file that has a URL. But it doesn’t show anything to the user. save-renata-comment.php does its work, then generates an HTTP header that says to the browser, “Sorry, you need to go to another place. Here is the URL.”

I don’t care whether you call it a page or not. The important thing to understand is that:

Some PHP programs don’t create HTML.

Being secure

There’s a problem with the code, though. Evil people could cause some problems.

  • They could enter so much data that the comments file gets too big, and uses all available disk space.

Actually, what they would do is write a program that repeatedly loaded our page and shoved a lot of data into the comments field. Do that 100,000 times, and the disk could get full.

  • They could enter JavaScript into the comments field. When someone else looked at that page, their code would execute.

Let’s make the following changes.

  • Limit the total size of the comments file to 50,000 characters.
  • Limit each individual comment to 2,000 characters.
  • Make sure any JavaScript that anyone types into the comment field can’t run.

Here’s the new version of save-renata-comment.php.

<?php
//Make sure the file exists.
touch('renata_comments.txt');
//Check the length of the comments file.
if ( filesize('renata_comments.txt') < 50000 ) {
  //Get the user's comment.
  //  Limit it to 2,000 characters.
  $comment = substr($_POST['comment'], 0, 2000);
  //Convert special characters to HTML entities.
  $comment = htmlentities($comment);
  //Append comment to the comments file.
  $f = fopen('renata_comments.txt', 'a');
  fwrite($f, "<p>$comment</p><hr>");
  fclose($f);
}
//Jump back to Renata's page.
header('location:renata.php');
?>

Figure 10. Secure version of save-renata-comment.php

Look at line 5:

if ( filesize('renata_comments.txt') < 50000 ) {

The filesize() function returns the number of bytes in the file. If this number is less than 50,000, the statements inside the braces ({}) will run. The braces extend from lines 5 to 15, and include the code that adds data to the comments file.

You can have as many statements as you want in the braces. You can have other if statements, if you want. We’ll see that later.

There is one problem with the filesize() function. It will cause an error if the file does not exist. That’s what line 3 does. The touch() function creates the file if it doesn’t exist.

Have a look at line 8:

$comment = substr($_POST['comment'], 0, 2000);

$_POST['comment'] returns the contents of the comment form field, as before. But rather than just putting that in $comment, something is done with it first.

substr() is a function that returns part of a string. It takes three arguments. The first is the string to process. The next two are numbers. One is the start position of the string. The other is the number of characters to return.

Here are some examples.

$x = 'Lion King';
print substr($x, 0, 3);

The first character is at position 0, not 1. So this says “Start at character 0, and return three characters. The result is:

Lio

How about this one?

print substr('Renata is cute', 2, 4);

This says “Start at character 2 and return 4 characters”. Character 2 is the third character (the first one is character 0). So the result is:

nata

One more.

$r = 'Rupert Giles';
$s = substr($r, 10, 8);
print $s;

This says “Start at the 11th character, and return 8 characters.” The 11th character is the “e” in “Giles.” The result will be:

es

substr() cannot return 8 characters, because there aren’t that many. So it returns what it can.

Back to the code. The line is:

$comment = substr($_POST['comment'], 0, 2000);

This says “Start at character 0 (the first character) and return the first 2,000 characters.” If there are, say, 23 characters, then $comment will contain all 23. If there are 4,321 characters, $comment will contain 2,000 characters.

The last thing we need to do is make sure that any JavaScript that the user types into the comment field will not run when the page is shown. There are various ways to do this; we’ll use the simplest.

For JavaScript to run, it has to have a <script> tag around it, like this:

<script>
alert("Evil JavaScript");
</script>

As you know, the < and the > have special meaning to the browser. They mean “Here is some HTML.” If you want to show a less than sign in your content, you need to replace < with &lt;. This is an HTML entity. It displays a less than sign, that isn’t interpreted as the start of a tag.

We can tell PHP to replace all the characters that have special meaning in HTML, like <, with their HTML entities. This is harmless:

&lt;script&gt;
alert("Evil JavaScript");
&lt;/script&gt;

It won’t be executed, because there is no < to indicate the start of an HTML tag.

Here’s what we do on line 10:

$comment = htmlentities($comment);

htmlentities() is a function that will go through a string, and replace all of the special characters with HTML entities.

Let’s say someone types this into the comment field:

Evil JavaScript

Figure 11. Evil JavaScript

Here is what would be stored into the comments file:

<p>&lt;script&gt;
alert(\&quot;Evil JavaScript\&quot;);
&lt;/script&gt;</p><hr>

The only HTML tags are the <p> and <hr> we added ourselves.

Summary

This lesson shows you how to add form data to a file. PHP gets the form data, opens the file in append mode, and writes the data.

We also looked as some security measures. We limited the total size of the comments file, the size of any one comment, and made sure that JavaScript typed into a comment would not execute.

What now?

Time for some exercises.