SimpleCMS Part 2 - Images

Handling User-Uploaded Images

Handling User-Uploaded Images

In part 1 of this series, we went over the basics of reading and writing information in a database, effectively creating a very simple content management system (CMS).

Due to the overwhelmingly positive response, I've decided to continue building on this CMS. I'll be writing a series of tutorials to cover new feature additions to our CMS.

Also, due to helpful criticism from experienced programmers, I'll be trying to address security concerns that were not taken into account in the original tutorial, leaving the CMS vulnerable to attack. However, I don't claim to be a security expert, so if you see room for improvement, let me know in the comments!

Uploading Images

Images are one of the most important parts of any website, so what would our CMS be without the ability to let users upload an image?

However, images are tough to manage when we're not directly controlling them; our users won't necessarily know how to resize their images to fit the allotted space on your page.

This can be a serious headache for a site admin, so we're not only going to allow users to upload images, but we're also going to resize (and resample) them to fit our needs.

View Demo

Building the Class

First and foremost, let's outline our class:

<?php

class simpleIMG {

  public $dims = array();
  public $path;

  public function verify() {

  }

  private function process() {

  }

}

?>

We're going to lay out two variables and two functions for this class. The first variable, $dims, is an array that will store our maximum image width and height.

Our second variable, $path, is going to store the path to a folder where we want user uploads to be stored.

Our two cleverly named functions serve to first verify that the file uploaded was, indeed, an image, then to process that image and store it on the server for later use.

Defining the Variables

Let's jump right in with our variables:

  public $dims = array('mh'=>200, 'mw'=>120);
  public $path = 'img/';

I've chosen to use 200px as the maximum height ("mh") and 120px as the maximum width ("mw") for this example.

Also, I created a folder named "img" to store the uploaded files. The path ("img/") has been stored in the variable $path so that we can pass it to our functions.

Verify the Upload

Before we do anything with the uploaded image, we want to be very sure it's an image. We want to avoid allowing users to upload malicious scripts or other such shenanigans, and we can do a relatively good job of checking that within PHP (additional precautions should be taken to be on the safe side; see the end of this tutorial for some further reading).

Another thing worth mentioning before we get into the code is the way that PHP file uploads work.

When a file is uploaded through an HTML form for use with PHP, the information is accessible through the special variable $_FILES, which puts relevant file data in an array. $_FILES is a multi-dimensional array, meaning multiple files can be stored in the variable array, each with its own array of information; we're only going pass the file array we need to verify(), contained in the variable $img.

I realize that sounds confusing, so think about it this way: $_FILES is like a big box, and inside it are smaller boxes, each filled with several items. We only need the items from one of those smaller boxes, so we're going to pull that small box out of the bigger box before processing it.

  public function verify($img) {
    if( getimagesize($img['tmp_name']) ) {
      if ( is_uploaded_file($img['tmp_name'])
      && $img['type'] == 'image/jpeg' || $img['type'] == 'image/pjpeg'
      || $img['type'] == 'image/gif' || $img['type'] == 'image/png'
      && $img['size'] <= 1024*1024*2 && $img['error'] == 0 ) {
        return $this->process($img,$type);
      } else return false;
    } else return false;
  }

In the first line, we check if the function getimagesize() returns true (successful) when we pass the uploaded file. If so, that means the file is an image and it's safe to go to our next step, in which we run another check with is_uploaded_file(), which makes sure the file was uploaded via HTTP POST.

At this point, we know with relative certainty that the file uploaded is an image, and also that the file was uploaded through our form. We finish the check by verifying that the file type is either JPEG, GIF, or PNG format and that it is less than 2MB in size before continuing, then making sure no errors occurred in the file upload.

After we've verified that the file is good to go, we can finally start working with it, so we call the function process(). We return the response of this function because it either returns the file path on success or false on failure.

Resize and Save the Image

  private function process($img, $type) {
    $name = $img['name'];
    $tmp = $img['tmp_name'];
    $size = $img['size'];
    $type = $img['type'];
    $err = $img['error'];
    $img_name = time() . '_' . md5($name);

At this point, we pull apart the $img variable to make it easier to work with. After simplifying the data, we also create a new name for the file.

Creating a new name for the file is an additional precaution against attacks, and it also prevents overwriting an image if two images are uploaded with the same filename. I opted to use the current Unix timestamp and the filename as it would be encoded by md5().

    switch ( $type ) {
      case 'image/jpeg':
      case 'image/pjpeg':
      default:
        $xt = '.jpg';
        $loc = $this->path . $img_name . $xt;
        $image_create_func = 'imagecreatefromjpeg';
        $image_func = 'imagejpeg';
        break;

      case 'image/gif':
        $xt = '.gif';
        $loc = $this->path . $img_name . $xt;
        $image_create_func = 'imagecreatefromgif';
        $image_func = 'imagegif';
        break;

      case 'image/png':
        $xt = '.png';
        $loc = $this->path . $img_name . $xt;
        $image_create_func = 'imagecreatefrompng';
        $image_func = 'imagepng';
        break;
    }

    if ( !move_uploaded_file($tmp,$loc) ) {
      return false;
      exit;
    }

After we have a filename, we set up a switch statement to determine what type of file we're dealing with.

For instance, if we have a file in JPEG format, we set the file extension ($xt) to ".jpg", create a file path to a JPEG image, and set a new variable called $image_create_func to "imagecreatefromjpg" and $image_func to "imagejpeg" (we'll get to that in just a second).

After we have our file path, we can move the uploaded file into the "img" directory. We accomplish this by using the function move_uploaded_file(). We also run a check here, saying that if the move fails, return false and stop executing this function.

    list($src_w,$src_h) = getimagesize($loc);
    if ( $src_w > $this->dims['mw']
    || $src_h > $this->dims['mh'] ) {
      if ( $src_h >= $src_w ) {
        $aspect = $this->dims['mh'] / $src_h;
      } else {
        $aspect = $this->dims['mw'] / $src_w;
      }
      $new_w = intval($src_w * $aspect);
      $new_h = intval($src_h * $aspect);
    } else {
      $new_w = $src_w;
      $new_h = $src_h;
    }

With our file now saved in a folder, we can move on to resizing the image for our site.

Our first call is to getimagesize() which will return an array of information about the file that was just uploaded, the first two values of which are the width and height of the image. Using a nifty language construct called list, we're able to assign two variables ($src_w and $src_h) to the first two values of the array.

With our width and height stored safely, we now check if the width and/or height is greater than our maximum width and height (remember $dims['mw'] and $dims['mh']?). If they are, we then check if the image is portrait or landscape and determine an aspect ratio that will allow us to reduce the image size proportionally.

After we've got our aspect ratio, we simply use it to multiply the width and height of the original image, giving us our proportional, smaller image.

    $src_img = $image_create_func($loc);
    $new_img = imagecreatetruecolor($new_w,$new_h);
    if ( imagecopyresampled($new_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, $src_w, $src_h) ) {
      imagedestroy($src_img);
      if ( $image_func($new_img, $loc, 90) ) {
        imagedestroy($new_img);
        return $loc;
      }
      else return false;
    }
    else return false;
  }

We're now ready to actually resize and resample the image. We start by calling the function we stored in $image_create_func earlier, which will create an editable image resource ($src_img). Next, we create a new image resource into which we'll be placing our reduced image ($new_img) using the function imagecreatetruecolor().

Where all the real work gets done is inside the next couple of lines. First imagecopyresampled() uses our two image resources to resample the image data from the original upload at the new size, then save that information in $new_img. If we're successful, we can safely free up the memory used by $src_img using imagedestroy().

We then call the function stored in $image_func to save the new image in the proper format and save it over the original image. If the function is successful, we free the resources and return the path of the image, stored in $loc.

If at any point something goes wrong, the function returns false, which, in this case, will just result in the image being skipped when we start saving everything into the database.

Integrating the Classes

To add the image class into our original CMS, all we need to do is add a couple lines. First, to add a form input to display_admin() we update the code as shown in bold:

  public function display_admin() {
    $form_action = htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES);
    return <<<ADMIN_FORM

    <form action="$form_action" method="post" enctype="multipart/form-data">
      <label for="title">Title:</label>
      <input name="title" id="title" type="text" maxlength="150" />
      <label for="img">Image:</label>
      <input name="img" id="img" type="file" />
      <label for="bodytext">Body Text:</label>
      <textarea name="bodytext" id="bodytext"></textarea>
      <input type="submit" value="Create This Entry!" />
    </form>

ADMIN_FORM;
  }

Then, in our write() function, we need to check if an image was uploaded (I chose to make the image optional in this instance), and if so, verify and process the file. We also need to modify our MySQL query to save the image path in the database. To do that, we add the following code (changes in bold) below our check for the title.

  public function write($p) {
    if ( $p['title'] )
      $title = mysql_real_escape_string(htmlentities($p['title'],ENT_QUOTES));
    if ( isset($_FILES['img']) ) {
      require_once 'simpleIMG.php';
      $simpleIMG = new simpleIMG();
      $img = $simpleIMG->verify($_FILES['img']);
    }
    if ( $p['bodytext'])
      $bodytext = mysql_real_escape_string(htmlentities($p['bodytext'],ENT_QUOTES));

    if ( $title && $bodytext ) {
      $created = time();
      $sql = "INSERT INTO testDB(title,bodytext,img,created) 
                      VALUES('$title','$bodytext','$img','$created')";
      return mysql_query($sql);
    } else {
      return false;
    }
  }

If we run the code as is, we should be able to successfully upload and store an image for use on our site. The last step is to display it!

Display the Image

Just as with saving the image, displaying it is a fairly simple task. Changes to display_public() are shown below in bold.

  public function display_public() {
    $q = "SELECT * FROM testDB ORDER BY created DESC LIMIT 3";
    $r = mysql_query($q);

    if ( $r !== false ) {
      while ( $a = mysql_fetch_assoc($r) ) {
        $title = stripslashes($a['title']);
        $bodytext = stripslashes($a['bodytext']);
        $imgLOC = stripslashes($a['img']);
        if ($imgLOC) {
          $img = <<<IMAGE_HTML

      <img src="$imgLOC" alt="$title" />
IMAGE_HTML;
        } else $img = NULL;

        $entry_display .= <<<ENTRY_DISPLAY

    <h2> $title </h2>
    <p>$img
      $bodytext
      <br />
    </p>

ENTRY_DISPLAY;
      }
    }

    if ( !$entry_display ) {
      $entry_display = <<<ENTRY_DISPLAY

    <h2> This Page Is Under Construction </h2>
    <p>
      No entries have been made on this page.
      Please check back soon, or click the
      link below to add an entry!
    </p>

ENTRY_DISPLAY;
    }
    $link = htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES);
    $entry_display .= <<<ADMIN_OPTION

    <p class="admin_link">
      <a href="$link?admin=1">Add a New Entry</a>
    </p>

ADMIN_OPTION;

    return $entry_display;
  }

And that's it! The code will now check if an image has been stored with the entry, then format it in the proper HTML and add it to the output. If not, nothing happens.

View the Demo | Download the Source

What Happens Next?

Continuing with this CMS, my next article will be on adding basic user authentication. Please let me know what else you'd like to see covered in this series by leaving a comment!

--

NOTE: This code is written for demonstration purposes only. I would strongly advise not using it for production websites without further testing. Allowing users to upload files to your server is a security risk, and as such, you should take special care when implementing code to allow such actions. I have covered some of the bases with security, but other issues may exist. Further reading on security risks with file uploads can be found here and here. Please read through these before implementing this code on your server to avoid potential security holes.

Posted Jan 17, 2009 by Jason Lengstorf.
This entry is filed under tutorial, php, cms, content management, and images.

Want more content like this? Subscribe for FREE!

Comments for This Entry

GravatarRodney04:07AM on January 10, 2009

Great tutorial, I was thinking about creating my own CMS, your tutorials convinced me of going at it.
I would like to see the possibility of using html-tags in the text, for linking purposes and the use of span-classes.
Also I think it would be a good idea to host part 1 on this site, not just on css-tricks.

GravatarMarkus11:36AM on January 10, 2009

great tutorial
but why you use always "<< what´s the different between a string and this above?

GravatarJason Lengstorf01:08PM on January 10, 2009

@Rodney:
If you wanted to allow certain tags, you could substitute

$bodytext = mysql_real_escape_string(strip_tags($p['bodytext'],'<a><span>'));

in the write() function. However, this opens up possibilities for XSS attacks, so you'll probably want to look into a way to further sanitize the allowed tags. There are some good ones in the comments on the PHP manual entry for strip_tags().

@Markus:
I use the '<<<' (HEREDOC) syntax because it allows me to format output to the browser. Other than that, there's not really too much of a difference between it and a standard string. You can read more about it in the PHP manual.

GravatarNick09:31PM on January 17, 2009

Hey Jason

I was wondering would it be possible to run a script like yours that allows a user to upload a image (say for a profile) then some where on another page call that image to be displayed (new user or something) and for that image to open up in a lightbox form with the original size.

I guess what I'm trying to get at is... How can I do something like your script but keep the original size of the image and make the thumbnail size of it?

GravatarAndrew04:38PM on January 18, 2009

Awesome tutorial! I've been working on a CMS for a little while, just for fun. These two tutorials are very simple and functional. I've replaced my code with this. Much nicer than what I had. Thanks again.

GravatarJason Lengstorf09:41PM on January 19, 2009

@Nick:
You could create an alias of the process function called thumb that would build a thumbnail of the image, then call both of them from the verify function. This is actually something I'm planning to tackle in a future post, so stay tuned!

@Andrew:
I'm glad it was useful for you!

GravatarJoshua Clayton08:39PM on February 05, 2009

yea, i'm not entirely a fan of heredoc, it's just seems foreign to me. i've used it with `cat` in unix before only because the command line requires it at times. i can see why it would be better looking to some but i'm always afraid of the string delimiter accidentally appearing in my string.
i remember reading somewhere that it was slightly faster than echo though (surprisingly enough to me...)

GravatarFranklin09:14AM on February 17, 2009

hey Jason,
The zip file does not download.
fix that please thank you

GravatarJason Lengstorf10:26AM on February 17, 2009

Franklin-

That's taken care of. For some reason the folder I had it in originally wanted to serve the file as plain text.

Sorry about that!

GravatarSean03:15AM on June 12, 2009

Hi Jason

These are great tutorials. Thanks!

There is just one thing I am wondering about. I have been playing about with part one of this tutorial.
If I enter a title and some text it works and puts it into the DB and the page pulls it back out to display.
The only thing is when I refresh the page it posts the information again.
How do you stop this?

Thanks

GravatarJason Lengstorf10:25AM on June 12, 2009

@Sean:
The information is resubmitted after a post if you reload. That's a browser feature. To avoid double-submission, click a link to get back to the page, or highlight the address in the address bar and press enter, rather than reloading.

Let me know if you have any further questions!

Gravatararafin09:50PM on June 25, 2009

Hi Jason,

Nice work. i already read part 1 and it was great, clear enough.

When will we get the next part?

GravatarJason Lengstorf09:58PM on June 25, 2009

@arafin:
I stopped work on this series, actually, because I am working with Apress Publishing to put out a book on building a CMS with PHP. It's supposed to come out this fall.

I'm sure I'll start yelling about it all over the Internet when it's available. :)

Thanks for reading!

GravatarMartin09:53AM on November 02, 2009

Can you please make a simple delete function? (Thinking about using this as an to-do list on my website)

GravatarJason Lengstorf11:32AM on November 02, 2009

@Martin:

To delete a file from your system, you would use the unlink() function. Be careful to verify that the file name is correct, because using unlink() can potentially delete more than you intended. There are good functions listed in the comments of the entry for unlink(): http://php.net/unlink

To delete the database entry, you would use the DELETE clause in MySQL with the WHERE clause to match up the entry ID.

Good luck!

Gravatarmoses03:45PM on January 01, 2010

Great tutorail thanks!

GravatarAbe05:59PM on January 14, 2010

The demo seems to time out with the following error:
Could not connect. Lost connection to MySQL server at 'reading initial communication packet', system error: 113

Another great tutorial though, thanks!

GravatarJason Lengstorf11:14AM on January 19, 2010

@Abe:
Thanks for pointing this out!

GravatarColor Experts International11:46PM on February 24, 2010

Very informative and nice tutorial....i found.....
I just bookmark this page for my review!!!!

GravatarG L03:57AM on March 07, 2010

The demo displays this error:
Building a Simple CMS - Part 2 - Handling Images

Could not connect. Can't connect to MySQL server on 'p41mysql51.secureserver.net' (4)

Gravatarangel08:05PM on March 25, 2010

Great tutoria

GravatarMike10:11PM on May 13, 2010

Hey Jason,

Great set of tutorials. I've learned a lot of the basics for PHP, but I'm trying to get into more OOP with it, but I've struggled to find good tutorials that have have practical objects for beginners. These have been great though. I'd love to see more tutorials for OOP.

Mike

GravatarQuintus09:16AM on August 03, 2010

You know what I like most here? The art. I like Jason Lengstorf 's funny little tennis ball head.

-Q

GravatarTim Novis04:24AM on August 10, 2010

This is a great tutorial, but I need to know how to make the "Post to site" link only available to the administrator, I can't seem to find the part where you talk about user authentication?

-Tim

GravatarjerryLee12:08AM on August 17, 2010

I am having a problem with my database reading back the information. I had to change $obj->table =
to testDB, as the script was reading the db1234566 as the db and trying to connect to it. Is there supposed to be a db name there also? This is great stuff, I just want to make sure I got it right.

Thanks bro,

look forward to the book!

GravatarJason Lengstorf10:15AM on August 17, 2010

@Tim:
For user authentication, check out the article series I did with Chris Coyier, Building a Web App from Scratch: http://css-tricks.com/examples/WebAppFromScratch/

@jerryLee:
If it's working, you've got it right. :)

Post a Comment

Want to show your face? Get a gravatar!

ALLOWED TAGS: <tt><strong><em>