How to convert the If-Modified-Since header to a date (and vice versa)?

Asked

Viewed 102 times

2

The header If-Modified-Since displays the following return format:

Wed, 21 Oct 2015 07:28:00 GMT

I need to compare this date that comes in this header item with the creation date of a file on my server, through the function filemtime($arquivo).

I want to do two things:

  • Transform the If-Modified-Since on a valid PHP date to compare it to the date returned in filemtime.

  • Transform the return of filemtime on a date in the same format as the header If-Modified-Since, to send as header Last-Modified denying.

So the question is:

  • Is there a standard in PHP to format the date equal to this header? And if you have a pattern, this date format has a name?

2 answers

2

Just use the format 'D, d M Y H:i:s T' (see the description of each field in documentation).

To transform the string into date, use date_create_from_format, and from the date returned by this function, use getTimestamp to be able to compare with the timestamp returned by filemtime:

$data = date_create_from_format('D, d M Y H:i:s T', 'Wed, 21 Oct 2015 07:28:00 GMT');
if ($data && $data->getTimestamp() > filemtime('arquivo')) {
    etc...
}

Already to convert the return of filemtime for the format in question, use gmdate, which is similar to date, however the date returned is in GMT (with date doesn’t work because it returns UTC instead of GMT). See the difference:

// testando com um arquivo modificado hoje
echo gmdate('D, d M Y H:i:s T', filemtime('arquivo')); // Wed, 10 Feb 2021 14:24:23 GMT
echo date('D, d M Y H:i:s T', filemtime('arquivo'));   // Wed, 10 Feb 2021 14:24:23 UTC

Another alternative is to use the constant DateTimeInterface::RFC7231 as format, which by explicitly putting "GMT" at the end, works both with date as to gmdate:

$format = DateTimeInterface::RFC7231;
$data = date_create_from_format($format, 'Wed, 21 Oct 2015 07:28:00 GMT');
// usar $data, etc

echo gmdate($format, filemtime('arquivo')); // Wed, 10 Feb 2021 14:24:23 GMT
echo date($format, filemtime('arquivo'));   // Wed, 10 Feb 2021 14:24:23 GMT

Going a little further...

How the question talks about the "value of header If-Modified-Since", I think it’s worth going a little deeper.

If we refer to RFC 7232 (HTTP Conditional Requests), we see that the header If-Modified-Since is defined as:

 If-Modified-Since = HTTP-date

An example of the field is:

 If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT

And the definition of HTTP-date is in RFC 7231 (my translation and emphases):

Before 1995, there were 3 different formats used by the servers for sending timestamps. For compatibility with old implementations, all 3 are defined here. The preferential format is ... a subset of what is specified in RFC5322:

HTTP-date    = IMF-fixdate / obs-date

An example of preferential format:

Sun, 06 Nov 1994 08:49:37 GMT    ; IMF-fixdate

Examples of formats obsolete:

Sunday, 06-Nov-94 08:49:37 GMT   ; obsolete RFC 850 format
Sun Nov  6 08:49:37 1994         ; ANSI C's asctime() format

That is, currently, one should give preference to the format quoted in the question (and that is defined in RFC5322). But RFC 7321 also cites that "A recipient that parses a timestamp value in an HTTP header field MUST Accept all three HTTP-date formats" (that is, even if the last 2 formats are obsolete, they must still be accepted).

Therefore, an alternative to accepting the 3 formats is to try one by one until it is possible to do the Parsing (or until all fail):

function parse($headerValue) {
    // formatos (dando preferência para o não-obsoleto)
    $formatos = [ DateTimeInterface::RFC7231, DateTimeInterface::RFC850, 'D M d H:i:s Y' ];
    foreach ($formatos as $formato) {
        $data = date_create_from_format($formato, $headerValue);
        if ($data) return $data;
    }
    return FALSE;
}

$formato_saida = DateTimeInterface::RFC7231;
$values = [ 'Wed, 21 Oct 2015 07:28:00 GMT', 'Sunday, 06-Nov-94 08:49:37 GMT', 'Sun Nov  6 08:49:37 1994' ];
foreach ($values as $val) {
    $data = parse($val);
    if ($data) {
        if ($data->getTimestamp() > filemtime('arquivo')) {
            // etc...
        }
        $saida = gmdate($formato_saida, $data->getTimestamp());
        echo "$val -> $saida\n";
    } else {
        echo "$val -> data inválida\n";
    }
}

To make the Parsing, I preferred the preferred format... indicated in RFC, and then I try the obsolete ones. Note that the second format is RFC850 (reading the year with 2 digits), instead of the COOKIE (which is almost the same, only he tries to read the year with 4 digits and 94 is interpreted as the year 94 instead of 1994). And for the third format, I couldn’t find a predefined constant that would fit, so I set up the format by hand.

Already to format the output, I used the RFC7231, that always puts "GMT" at the end (the others put other values, such as "UTC" or "+00:00"). I chose to use this format, because it is the only one that RFC defines as not being obsolete (i.e., to receive I accept any format, but when sending, I use the "preferred" non-obsolete). The exit code above is:

Wed, 21 Oct 2015 07:28:00 GMT -> Wed, 21 Oct 2015 07:28:00 GMT
Sunday, 06-Nov-94 08:49:37 GMT -> Sun, 06 Nov 1994 08:49:37 GMT
Sun Nov  6 08:49:37 1994 -> Sun, 06 Nov 1994 08:49:37 GMT

A simpler alternative

The above options are for the case of you in addition to the comparison with filemtime, also need to manipulate the date in other ways (since date_create_from_format returns a DateTime).

But if you only want the value of the timestamp to be able to compare with the return of filemtime, can use strtotime, recognizing the 3 formats defined in RFC:

$formato_saida = DateTimeInterface::RFC7231;
$values = [ 'Wed, 21 Oct 2015 07:28:00 GMT', 'Sunday, 06-Nov-94 08:49:37 GMT', 'Sun Nov  6 08:49:37 1994' ];
foreach ($values as $val) {
    $timestamp = strtotime($val);
    if ($timestamp === FALSE) {
        echo "$val -> data inválida\n";
    } else {
        // comparação direta
        if ($timestamp > filemtime('arquivo')) {
            // etc...
        }

        // formatar para o formato preferencial não-obsoleto
        $saida = gmdate($formato_saida, $timestamp);
        echo "$val -> $saida\n";
    }
}

The difference, of course, is that strtotime accepts several other formats (some even half "esoteric", as 'last day of february this year'), then it depends on how rigid you want to be: if you want to accept only the 3 formats indicated by RFC, use the function parse defined above (which only specifically accepts those formats).

But if the input is controlled (for example, you guarantee that the date is obtained from header - which in that case will be available in $_SERVER['HTTP_IF_MODIFIED_SINCE']) and that will not be in an out of standard format, use strtotime simplifies things.


For more details, see "Handling If-modified-Since header in a PHP-script".

  • A detail, if the value received by the header is invalid DateTime::createFromFormat (procedural: date_create_from_format) will return FALSE, then it would be good to do the IF this way: if ($data && $data->getTimestamp() > filemtime('arquivo')) {

2

Actually to send the Last-Modified doesn’t need the If-Modified-Since, the If-Modified-Since already is consequence of Last-Modified, then if the claim is to send only the filemtime($arquivo) as Last-Modified for the HTTP response, one gmdate with the parameters in the correct order, because the filemtime already working with Unix time, something like:

$modified = filemtime(<arquivo>);

header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified) . ' GMT');

You can escape the GMT with \G\M\T or use the T, but it is irrelevant, the expected value should be GMT itself, do as you find most pleasant.

The If-Modified-Since being consequence of the latter Last-Modified will be used if you need to compare what you have on the server with what you have on the client, so that’s why in the next requests to the same address you can work this value, comparing you can decrease the load of downloads, using the code 304:

$modified = filemtime(<arquivo>);

if (<If-Modified-Since> >= $modified) {
    http_response_code(304);
    exit;
}

header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified) . ' GMT');

readfile(<arquivo>);

In this pseudocode we send the file to the HTTP response only if the file has been modified, otherwise the browser (or other client system working with HTTP) should understand that nothing has changed and therefore should reuse the existing cache.

It is also possible to work together with E-tag or just use E-tag, is not necessarily obliged to work with both, the idea of the E-tag is to have a way to confirm that there was no change at all, it can contain the version or a "hash" (ASCII value), there is the indicated E-tag if you are using a weak validator or nay, but all this is another story and goes far beyond the question.

Back to focus. Only if the goal is really to use the 304 Not Modified that the following code will be useful:

$data = date_create_from_format('D, d M Y H:i:s T', <If-Modified-Since>);

if ($data && filemtime(<arquivo>) > $data->getTimestamp()) {
    etc...
}

The purpose of this would be to validate the value sent in the HTTP request header, since HTTP request headers can be manipulated, so an idea of semi-functional implementation would be:

$modified = filemtime($arquivo);

// Checa se o header foi enviado pelo cliente e então tenta fazer o parse
$data = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? date_create_from_format('D, d M Y H:i:s T', $_SERVER['HTTP_IF_MODIFIED_SINCE']) : null;

if ($data && $data->getTimestamp() >= $modified) {
    http_response_code(304);
    exit;
}

header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modified) . ' GMT');

readfile($arquivo);

By logic it has to be >= and not > because if the file has not changed the values will be "equal".

This whole implementation can be done even in "frameworks" that has its own HTTP interface and preview the request header If-Modified-Since in some kind of middleware, or maybe the framework itself or the HTTP server validates this, but I can’t say for all technologies, I just want to say that depending on the scenario maybe validation can be expendable at this point of execution if another technology already implements.

Remember that in the example I used the function readfile, but as far as I used it was problematic with large files, the ideal for large files would be to use fopen + flush, but this is a matter of optimization, which goes far beyond what is asking.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.