How to divide a list of results into pages in PHP

March 9th, 2009

There comes a time when a list of records output by a MySQL query becomes too long to realistically display on a single page, and the programmer wants to break it up into pages. It might be log activity, forum posts or whatever, but the principle is the same – we only want to view a manageable section of the entire record list.

Let’s assume we’re dealing with a set of forum posts in a thread. Your query would take a unique number for the thread ($threadId) and you would pull out a list of posts like this:

<php
 $postSQL="SELECT * FROM posts WHERE (THREAD=$threadId) ORDER BY ID";
 $posts = mysql_query($postSQL) or die(mysql_error());
 while($post=mysql_fetch_array($posts)) {
  // display posts here
 }
?>

This would return all of the posts for this thread in order of creation – ID being the master key for this table.

To split this into pages, we first need to decide how many posts we want per page. Once we have decided on this, we can see how many lines our mysql_query(), above, returns and calculate the number of pages. After that we can decide which page to show and calculate which posts go on that page.

Here’s the altered code:

<?php
 $postSQL="SELECT * FROM posts WHERE (THREAD=$threadId) ORDER BY ID";
 $posts = mysql_query($postSQL) or die(mysql_error());

 $postCount   =mysql_affected_rows();
 $postsPerPage=25;
 $pageCount   =ceil($postCount/$postsPerPage);
 $pageNum     =min(max(1, (int)$_GET['page']), $pageCount);
 $firstPost   =($pageNum-1)*$postsPerPage;
 $lastPost    =min($firstPost+$postsPerPage, $postCount);

 $posts = mysql_query("$postSQL LIMIT $firstPost, $postsPerPage") 
 or die(mysql_error());
 while($post=mysql_fetch_array($posts)) {
  // display posts here
 }
?>

As you can see, we call mysql_query() twice now – the first time to get the total number of posts and the second time to get the subset of those posts once we have decided which ones we want. The highlighted green section shows what has been added, and I’ll go through them here:

$postCount =mysql_affected_rows();

Records the total number of posts returned. We need this so we can calculate how many pages we have in total using…

$postsPerPage=25;

This is how many posts we want on a single page. You can store this in a table for easy altering, or allow it to be overwritten from the URL so that users can decide on their own page sizes.

$pageCount =ceil($postCount/$postsPerPage);

The number of pages is the number of posts divided by the number of posts per page, and rounded up, so ‘30 posts at 25 posts per page’ would return two pages.

$pageNum =min(max(1, (int)$_GET['page']), $pageCount);

Slightly more tricky to read, this line limits the number of the page to be displayed between 1 and the total number of pages, taking into account any page number specified in the URL with ‘&page=’, even if a value is not given.

$firstPost =($pageNum-1)*$postsPerPage;

We need to know the record position – not the ID – of the first post so that we can tell MySQL where to start counting from. This number starts at zero whereas your ID would start at one.

$lastPost =min($firstPost+$postsPerPage, $postCount);

We don’t actually need $lastPost for anything in this example, but it’s nice if you want to display a message along the lines of “Showing posts 25 to 50 of 376″. This value is the actual ID of the last post on the page, so it starts at one and never goes above $postCount.

$posts = mysql_query(“$postSQL LIMIT $firstPost, $postsPerPage”)
or die(mysql_error());

Finally we run your original SQL statement – here held in $postSQL – a second time but with our calculated limits applied.

PHP’s number_format(); breaks my form. Oy!

August 1st, 2006

Interesting PHP buglet of the month. I had a call from a client saying that the new prices he’d entered into his website’s admin area were coming back as £1 and £2 instead of the thousands that they should have been. I asked a few questions but it suddenly occurred to me that if you entered a comma in a number value, that would screw things up.

The client, of course, insisted that they were not entering commas into anything, but it transpires that when you pull a value from the DB and run it through number_format() (to get the value to two decimal places and present it as a currency) it actually seperates the groups of three digits with a comma. So:

    $price=number_format(123450/100,2);

returns “1,234.50″ rather than the “1234.50″ I was expecting.

This is all well and good until you actually save it in a form, whereby the comma ends up sending two seperate values for the same variable. Since only the first is used, the PHP on the receiving end sees ‘1′ instead of ‘1234.50′ and all hell breaks loose.

Well not really – the problem was spotted after only four items had gone wonky and before they actually had any customers, so nothing was lost on this little voyage of discovery. I wonder if our contract contains a clause absolving us of responsibility for stuff like this?

PHP mail() Email Injection Attack Allows Spammers to Send Email

February 14th, 2006

I was at a client’s last week and the guy who handles the email mentioned that they were getting a lot of spam from the same email addresses, all of which were at the default name of our server. I had a quick look and, sure enough, email was being sent through a mail script on the back end of my client’s Contact Us page. Because the To: address is hardwritten into the script, my client received copies of the spam as well as the injected recipients.

How the Email Injection Attack works…

The injection attack works with any mailing script that uses the PHP mail() function. If the script accepts a name and email address, and formats those into the From: field of your email, then they can be used to insert extra email addresses anonymously.

Ordinarily you would take a name ($name) and an email address ($email) and create a string like so:

    $headers="From: $name <$email>\r\n";

which creates:

    From: MyName <user@domain.tld>

followed by a ‘\r\n’, or Character Return+Line Feed.

Using a form on another server, or even the existing form if there’s no client-side length checking, the spammer submits the following as $name:

    blah%0ASubject:SpammerSubject%0Abcc:victim@domain2.tld

PHP’s mail() function puts these into the headers as usual, but the headers now contain an extra Subject: and Bcc: field that were not there before. Either the $name or $email variables (in our example) can be used for this, and there’s nothing to stop them attaching files using MIME.

So How to I Protect Against Email Header Injection Attacks?

Thankfully, protecting yourself is quite easy – probably the easiest way is for you to check the vulnerable fields for illegal content on receipt by your processing script. My approach was to write a simple function using a regular expression, thus:

    <?php

    function spamcheck($spammed_field) {

    $spammed_field=strtolower($spammed_field);

    if((eregi("cc: ",$spammed_field))||(eregi("subject: ",$spammed_field))) {

    $spamhost=$_SERVER['REMOTE_HOST'];

    $spamrefr=$_SERVER['HTTP_REFERER'];

    $spamaddr=$_SERVER['HTTP_X_FORWARDED_FOR'];

    if(strlen($spamaddr)<7) { $spamaddr=$_SERVER['HTTP_CLIENT_IP']; }

    if(strlen($spamaddr)<7) { $spamaddr=$_SERVER['REMOTE_ADDR']; }

    $thisfile=$_SERVER['SCRIPT_NAME'];

    $spamtext="FILE: $thisfile \nFROM: $spamrefr \nADDR: $spamaddr \nHOST: $spamhost \nINFO:\n$spammed_field\n";

    mail("spamcheck@domain.tld","ALERT: $spamaddr",$spamtext,"From: SpamCheck <spamcheck@domain.tld>\r\n");

    die();

    }

    }

    ?>

All you have to do to check the vulnerable fields is include the function and call it for each one:

    include('spamcheck.php');
    spamcheck($name);
    spamcheck($email);

If either ‘Cc:’, ‘Bcc:’ or ‘Subject:’ is found somewhere it shouldn’t be, the script generates an email containing the name of the script and the spammer’s IP address, sends it to spamcheck@domain.tld and promptly dies.