Caching images in browser with URL parameters and XSendFile
Asked Answered
C

4

8

I am trying to set up a site so that users only have access to their own images and audio files. To to this I am using variables in the URL such as:

 <img src="/public/get_file.php?type=image&file=pic001.png" />

On the front-end I am using React JS (not sure if that is important to this issue). But on the backend, the PHP script will validate that the user is logged in (just by checking a simple SESSION variable) and look for that file in the user's data directory (based on the user id in their SESSION variable). If it exists it will return it to the user using XSendFile.

The problem I am having is that every time that the user tries to access the files there is a bit of a delay before they load. This is telling me that they probably are not being cached by the browser.

Why are the files not getting cached? Does it have to do with the URL parameter or the use of PHP/XSendFile?

What can I do to allow my files (images/audio) to be cached?

Update

As requested here is my get_file.php:

<?php

        session_start();

        if (empty($_SESSION['auth']['id'])){
                exit;
        }

        require_once("../../api_modules/settings.php");

        $developer_id = $_SESSION['auth']['id'];

        $type = preg_replace('/[^-a-zA-Z0-9_]/', '', $_GET['type']);
        $file = preg_replace('/[^-a-zA-Z0-9_\.]/', '', $_GET['file']);

        $file = preg_replace('/[\.]{2,}/', '', $file);

        $project_id = preg_replace('/[^0-9]/', '', $_GET['project_id']);
        $developer_id = preg_replace('/[^0-9]/', '', $developer_id);

        if ($type == "image")
                $file = FILEPATH ."files/$developer_id/$project_id/images/thumbs/88x88/$file";
        else if ($type == "full_image")
                $file = FILEPATH ."files/$developer_id/$project_id/images/originals/$file";
        else if ($type == "audio"){
                $file = FILEPATH ."files/$developer_id/$project_id/audio/$file";
        }
        else {
                exit;
        }


        header("X-Sendfile: $file");
        header("Content-type: application/octet-stream");
        header('Content-Disposition: attachment; filename="' . basename($file) . '"');
Chambliss answered 10/3, 2019 at 18:17 Comment(7)
can you post the response of the image request from the Chrome dev tools network tab?Carycaryatid
@Carycaryatid While I do use the console often, I am not too used to using the network tab (response) section. However, I do see my request, but the response says: "The request has no response data available".Chambliss
just for clarification, you want the browser to be able to cache the images?Testaceous
@Chambliss Which HTTP server are you using?Brimmer
@TomaszKajtoch I do not fully understand your question. My question is tagged as Apache. Is there something else you are looking for?Chambliss
@Testaceous Yes, currently if I go to one page it will load images, if I leave to another page, and then come back to the first page it has to reload the images again.Chambliss
Could you post your get_file.php code please?Meyerbeer
M
0

I think this is a duplicate.

But since there is a bounty lets try to adapt your code together with the suggested solution there.

You need to check that Apache has mod_expires and mod_headers enabled and working properly.

And then before you start real output check if file was modified:

...
$last_modified_time = filemtime($file); 
$etag = md5_file($file);
// always send headers
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_modified_time)." GMT"); 
header("Etag: $etag"); 
// exit if not modified
if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified_time || 
    @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { 
    header("HTTP/1.1 304 Not Modified"); 
    exit; 
} else {
    header("X-Sendfile: $file");
    header("Content-type: application/octet-stream");
    header('Content-Disposition: attachment; filename="' . basename($file) . '"');
}
Meyerbeer answered 23/3, 2019 at 2:53 Comment(3)
I am not sure I am fully understanding, but does the web browser request the file, but then if it receives the 304 message, it cancels its request and uses its cache instead?Chambliss
Yes. That is how browser can reuse cached http responsesMeyerbeer
Likely won't get time to fully test this by tomorrow, but this seems like the best answer to me.Chambliss
H
0

Try to use this kind of header (PHP code) with for example header('Cache-Control: must-revalidate'); for download a private files

$mime_types = array(
        'pdf' => 'application/pdf',
        'txt' => 'text/plain',
        'html' => 'text/html',
        'exe' => 'application/octet-stream',
        'zip' => 'application/zip',
        'doc' => 'application/msword',
        'xls' => 'application/vnd.ms-excel',
        'ppt' => 'application/vnd.ms-powerpoint',
        'gif' => 'image/gif',
        'png' => 'image/png',
        'jpeg' => 'image/jpg',
        'jpg' => 'image/jpg',
        'php' => 'text/plain'
        );

if ($mimeType == '') {
    $file_ext = strtolower(substr(strrchr($file_path.$file_name_gen, '.'), 1));
    if(array_key_exists($file_ext, $mime_types)) $mime_type = $mime_types[$file_ext];
    else $mime_type = 'application/force-download';
    }


ob_start();
header('Content-Disposition: attachment; filename="' . $file_name_gen . '"');
header('Content-Type: '.$mime_type);
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file_path.$file_name));
ob_clean();

flush();
readfile($file_path.$file_name);

And use Cache control with a best option for your solution like

Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache 
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached

BTW. $file_name_gen is a genuine file name on the server and $file_name is a file name users see for more file privacy.

Huckaby answered 16/3, 2019 at 16:23 Comment(1)
Cache-Control: private to avoid caching by proxy-servers.Macedo
P
0

For the record, this is mostly due to the fact the default value of session.cache_limiter is nocache.

See http://php.net/manual/en/function.session-cache-limiter.php on how to set a different value that fits your needs.

Poundage answered 20/3, 2019 at 1:53 Comment(0)
M
0

How do you generate the images? Do you have a public local directory or it is loaded by a CDN (remotely)?

Maybe it can help: HOW TO CACHE IMAGES GENERATED BY PHP

Maddock answered 21/3, 2019 at 13:0 Comment(3)
The images are stored in directories on the same server. Each user has their own user directory labeled by their user id.Chambliss
Ok, so it's easier to manage cache with local images. You can try the solution on link and see if it'll work. But what happen if someone try to access the images directly?Maddock
They cannot access images directly as they are not located in the web accessible folder.Chambliss
M
0

I think this is a duplicate.

But since there is a bounty lets try to adapt your code together with the suggested solution there.

You need to check that Apache has mod_expires and mod_headers enabled and working properly.

And then before you start real output check if file was modified:

...
$last_modified_time = filemtime($file); 
$etag = md5_file($file);
// always send headers
header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_modified_time)." GMT"); 
header("Etag: $etag"); 
// exit if not modified
if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified_time || 
    @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { 
    header("HTTP/1.1 304 Not Modified"); 
    exit; 
} else {
    header("X-Sendfile: $file");
    header("Content-type: application/octet-stream");
    header('Content-Disposition: attachment; filename="' . basename($file) . '"');
}
Meyerbeer answered 23/3, 2019 at 2:53 Comment(3)
I am not sure I am fully understanding, but does the web browser request the file, but then if it receives the 304 message, it cancels its request and uses its cache instead?Chambliss
Yes. That is how browser can reuse cached http responsesMeyerbeer
Likely won't get time to fully test this by tomorrow, but this seems like the best answer to me.Chambliss

© 2022 - 2024 — McMap. All rights reserved.