Michel
Michel

Reputation: 23615

Serve image with PHP script vs direct loading an image

I want to monitor how often some external images are loaded. So my idea is instead of giving a uri directly like this:

www.site.com/image1.jpg

I can create a PHP script which reads the image, so I built a PHP file and my HTML would look like this:

<img src="www.site.com/serveImage.php?img=image1.jpg">

but I don't know how to read the image from disk and return it. Would I return a byte array or set the content type?

Kind regards, Michel

Upvotes: 25

Views: 47259

Answers (9)

FontFamily
FontFamily

Reputation: 386

There are a lot of good answers above, but none of them provide working code that you can use in your PHP app. I've set mine up so that I lookup the name of the image in a database table based off a different identifier. The client never sets the name of the file to download as this is a security risk.

Once the image name is found, I explode it to obtain the extension. This is important to know what type of header to serve based off the image type (i.e. png, jpg, jpeg, gif, etc.). I use a switch to do this for security reasons and to convert jpg -> jpeg for the proper header name. I've included a few additional headers in my code that ensure the file is not cached, that revalidation is required, to change the name (otherwise it will be the name of the script that is called), and finally to read the file from the server and transmit it.

I like this method since it never exposes the directory or actual file name. Be sure you authenticate the user before running the script if you are trying to do this securely.

$temp = explode('.', $image_filename);
$extension = end($temp);    // jpg, jpeg, gif, png - add other flavors based off your use case

switch ($extension) {
    case "jpg":
        header('Content-type: image/jpeg');
        break;
    case "jpeg":
    case "gif":
    case "png":
        header('Content-type: image/'.$extension);
        break;
    default:
        die;    // avoid security issues with prohibited extensions
}

header('Content-Disposition: filename=photo.'.$extension);
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
readfile('../SECURE_DIRECTORY/'.$image_filename);

PHP 8 lets you use the match feature, which will further optimize the code by getting rid of the switch and ugly looking nested cases.

Upvotes: 1

htmlcoderexe
htmlcoderexe

Reputation: 173

I serve my images with readfile as well, but I have gone the extra mile both for security and extra functionality.

I have a database set up which stores the image id, its dimensions and file extension. This also means that images need to be uploaded (allowing optional resizing), so I only use the system for content and not images needed for the website itself (like backgrounds or sprites).

It also does a very good job at making sure you can only request images.

So, for serving the simplified workflow would be like this (cannot post production code here):

1) get the ID of the requested image

2) Look it up in the database

3) Throw headers based on the extension ("jpg" gets remapped to "jpeg" on upload)

4) readfile("/images/$id.$extension");

5) Optionally, protect /images/ dir so it cannot be indexed (not a problem in my own system as it maps URLS like /image/view/11 to something like /index.php?module=image&action=view&id=11)

Upvotes: 2

OIS
OIS

Reputation: 10033

Sending images through a script is nice for other things like resizing and caching on demand.

As answered by Pascal MARTIN the function readfile and these headers are the requirements:

  • Content-Type
    • The mime type of this content
    • Example: header('Content-Type: image/gif');
    • See the function mime_content_type
    • Types
      • image/gif
      • image/jpeg
      • image/png

But beside the obvious content-type you should also look at other headers such as:

  • Content-Length
    • The length of the response body in octets (8-bit bytes)
    • Example: header('Content-Length: 348');
    • See the function filesize
    • Allows the connectio to be better used.
  • Last-Modified
    • The last modified date for the requested object, in RFC 2822 format
    • Example: header('Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT');
    • See the function filemtime and date to format it into the required RFC 2822 format
      • Example: header('Last-Modified: '.date(DATE_RFC2822, filemtime($filename)));
    • You can exit the script after sending a 304 if the file modified time is the same.
  • status code
    • Example: header("HTTP/1.1 304 Not Modified");
    • you can exit now and not send the image one more time

For last modified time, look for this in $_SERVER

  • If-Modified-Since
    • Allows a 304 Not Modified to be returned if content is unchanged
    • Example: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
    • Is in $_SERVER with the key http_if_modified_since

List of HTTP header responses

Upvotes: 42

Samer Ata
Samer Ata

Reputation: 1027

Instead of changing the direct image url in the HTML, you can put a line in the Apache configuration or .htaccess to rewrite all the requests of images in a directory to a php script. Then in that script you can make use of the request headers and the $_server array to process the request and serve the file.

First in your .htaccess:

RewriteRule ^(.*)\.jpg$ serve.php [NC]
RewriteRule ^(.*)\.jpeg$ serve.php [NC]
RewriteRule ^(.*)\.png$ serve.php [NC]
RewriteRule ^(.*)\.gif$ serve.php [NC]
RewriteRule ^(.*)\.bmp$ serve.php [NC]

The script serve.php must be in the same directory as .htaccess. You will probably write something like this:

<?php
$filepath=$_SERVER['REQUEST_URI'];
$filepath='.'.$filepath;
if (file_exists($filepath))
{
touch($filepath,filemtime($filepath),time()); // this will just record the time of access in file inode. you can write your own code to do whatever
$path_parts=pathinfo($filepath);
switch(strtolower($path_parts['extension']))
{
case "gif":
header("Content-type: image/gif");
break;
case "jpg":
case "jpeg":
header("Content-type: image/jpeg");
break;
case "png":
header("Content-type: image/png");
break;
case "bmp":
header("Content-type: image/bmp");
break;
}
header("Accept-Ranges: bytes");
header('Content-Length: ' . filesize($filepath));
header("Last-Modified: Fri, 03 Mar 2004 06:32:31 GMT");
readfile($filepath);

}
else
{
 header( "HTTP/1.0 404 Not Found");
 header("Content-type: image/jpeg");
 header('Content-Length: ' . filesize("404_files.jpg"));
 header("Accept-Ranges: bytes");
 header("Last-Modified: Fri, 03 Mar 2004 06:32:31 GMT");
 readfile("404_files.jpg");
}
/*
By Samer Mhana
www.dorar-aliraq.net
*/
?>

(This script can be improved!)

Upvotes: 9

J. Bruni
J. Bruni

Reputation: 20492

I use the "passthru" function to call "cat" command, like this:

header('Content-type: image/jpeg');
passthru('cat /path/to/image/file.jpg');

Works on Linux. Saves resources.

Upvotes: 12

dlchambers
dlchambers

Reputation: 3752

Also, if you want to the user to see a real filename instead of your scriptname when the user RMC's on the image and selects "Save As", you'll need to also set this header:

header('Content-Disposition: filename=$filename');

Upvotes: 5

nickf
nickf

Reputation: 546025

You're probably better off examining your server access logs for this. Running all images through php might put a bit of load on your server.

Upvotes: 2

Pascal MARTIN
Pascal MARTIN

Reputation: 400962

To achieve something like this, your script will need to :

  • send the right headers, which depend on the type of the image : image/gif, image/png, image/jpeg, ...
  • send the data of the image
  • making sure nothing else is sent (no white space, no nothing)

This is done with the header function, with some code like this :

header("Content-type: image/gif");

Or

header("Content-type: image/jpeg");

or whatever, depending on the type of the image.


To send the data of the image, you can use the readfile function :

Reads a file and writes it to the output buffer.

This way, in one function, you both read the file, and output its content.


As a sidenote :

  • you must put some security in place, to ensure users can't request anything they want via your script : you must make sure it only serves images, from the directory you expect ; nothing like serveImage.php?file=/etc/passwd should be OK, for instance.
  • If you're just willing to get the number of times a file was loaded each day, parsing Apache's log file might be a good idea (via a batch run by cron each day at 00:05, that parses the log of the day before, for instance) ; you won't have real-time statistics, but it will require less resources on your server (no PHP to serve static files)

Upvotes: 23

mck89
mck89

Reputation: 19231

You must set the content type:

header("Content-type: image/jpeg");

Then you load the image and output it like this:

$image=imagecreatefromjpeg($_GET['img']);
imagejpeg($image);

Upvotes: 11

Related Questions