Change record status: 
Project: 
Introduced in branch: 
8.7.x
Introduced in version: 
8.7.0
Description: 

Only sites/modules doing advanced things with datetime are affected; nothing in core today is affected.

The solved problem

Datetime (@FieldType=datetime) fields are stored in UTC, but when retrieving not just the value but when interacting with their value property (which is a @DataType=datetime_iso8601 instance), you can interact with it not only through the getValue() method mandated by \Drupal\Core\TypedData\PrimitiveInterface, but also through the getDateTime() method mandated by \Drupal\Core\TypedData\Type\DateTimeInterface.

A few factors played a role in this bug having existed for many years:

  1. datetime fields are stored in the database without a timezone offset.
  2. The getValue() method simply returns the data stored in the database. So you'd get the data as stored in the database: a datetime string without a timezone offset.
  3. The getDateTime() method needed to convert this (offsetless) datetime string into a \Drupal\Core\Datetime\DrupalDateTime object
  4. When passing offsetless datetime strings to the \Drupal\Core\Datetime\DrupalDateTime constructor, the default timezone (date_default_timezone_get()) is used, which is set to the site's timezone, or the currently authenticated user's preferred timezone, if any
  5. In Drupal core, there are zero calls to DateTimeIso8601::getDateTime(). Only tests are calling it.
  6. Result: datetime strings that are actually in UTC are assumed to be in the current timezone and hence are interpreted incorrectly.

It's thanks to the API-First Initiative adding a normalizer to correctly and consistently normalize datetime information that we discovered this at all.

Concrete example

Assumption
Your site is in the Australia/Sydney timezone.
2018-12-13T17:00:00 is stored in the database
Europe/Brussels is UTC+1, Australia/Sydney is UTC+10, so 10 hour time difference
var_dump($typed_data->getDateTime()->format(\DateTime::RFC3339));
var_dump($typed_data->getDateTime()->setTimezone(new \DateTimeZone('Europe/Brussels'))->format(\DateTime::RFC3339));
Before
2018-12-13T17:00:00+11:00
2018-12-13T07:00:00+01:00

Note how the stored datetime information is interpreted as being in Australia/Sydney.

After
2018-12-13T17:00:00+00:00
2018-12-13T18:00:00+01:00

Note how the stored datetime information is interpreted as being in UTC.

Impacts: 
Module developers