diff --git a/core/includes/common.inc b/core/includes/common.inc index b3dc9cb..81aeded 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5,6 +5,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Database\Database; use Drupal\Core\Template\Attribute; @@ -1975,26 +1976,12 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL break; } - // Create a DateTime object from the timestamp. - $date_time = date_create('@' . $timestamp); - // Set the time zone for the DateTime object. - date_timezone_set($date_time, $timezones[$timezone]); - - // Encode markers that should be translated. 'A' becomes '\xEF\AA\xFF'. - // xEF and xFF are invalid UTF-8 sequences, and we assume they are not in the - // input string. - // Paired backslashes are isolated to prevent errors in read-ahead evaluation. - // The read-ahead expression ensures that A matches, but not \A. - $format = preg_replace(array('/\\\\\\\\/', '/(? $langcode); + return $date_time->format($format, $settings); } /** @@ -4974,7 +4961,8 @@ function drupal_page_set_cache($body) { $cache->data['headers'][$header_names[$name_lower]] = $value; if ($name_lower == 'expires') { // Use the actual timestamp from an Expires header if available. - $cache->expire = strtotime($value); + $date = new DrupalDateTime($value); + $cache->expire = $date->getTimestamp(); } } diff --git a/core/lib/Drupal/Component/Datetime/DateTimeBase.php b/core/lib/Drupal/Component/Datetime/DateTimeBase.php new file mode 100644 index 0000000..35f349c --- /dev/null +++ b/core/lib/Drupal/Component/Datetime/DateTimeBase.php @@ -0,0 +1,703 @@ + 2014, 'month => 4). + * Defaults to 'now'. + * @param mixed $timezone + * PHP DateTimeZone object, string or NULL allowed. + * Defaults to NULL. + * @param string $format + * PHP date() type format for parsing the input. This is recommended + * to use things like negative years, which php's parser fails on, or + * any other specialized input with a known format. If provided the + * date will be created using the createFromFormat() method. + * Defaults to NULL. + * @see http://us3.php.net/manual/en/datetime.createfromformat.php + * @params array $settings + * - boolean $validate_format + * The format used in createFromFormat() allows slightly different + * values than format(). If we use an input format that works in + * both functions we can add a validation step to confirm that the + * date created from a format string exactly matches the input. + * We need to know if this can be relied on to do that validation. + * Defaults to TRUE. + * - string $locale + * A locale name, using the pattern specified by the + * intlDateFormatter class. Used to control the result of the + * format() method if that class is available. Defaults to NULL. + * - string $calendar + * A calendar to use for the date, Defaults to NULL. + * - boolean $debug + * Leave evidence of the input values in the resulting object + * for debugging purposes. Defaults to FALSE. + * + * @TODO + * Potentially there will be additional ways to take advantage + * of locale and calendar in date handling in the future. + */ + public function __construct($time = 'now', $timezone = NULL, $format = NULL, $settings = array()) { + + // Unpack settings. + $this->validate_format = !empty($settings['validate_format']) ? $settings['validate_format'] : TRUE; + $this->locale = !empty($settings['locale']) ? $settings['locale'] : NULL; + $this->calendar = !empty($settings['calendar']) ? $settings['calendar'] : NULL; + + // Store the original input so it is available for validation. + $this->input_time_raw = $time; + $this->input_timezone_raw = $timezone; + $this->input_format_raw = $format; + + // Massage the input values as necessary. + $this->prepareTime($time); + $this->prepareTimezone($timezone); + $this->prepareFormat($format); + + // Create a date as a clone of an input DateTime object. + if ($this->inputIsObject()) { + $this->constructFromObject(); + } + + // Create date from array of date parts. + elseif ($this->inputIsArray()) { + $this->constructFromArray(); + } + + // Create a date from a Unix timestamp. + elseif ($this->inputIsTimestamp()) { + $this->constructFromTimestamp(); + } + + // Create a date from a time string and an expected format. + elseif ($this->inputIsFormat()) { + $this->constructFromFormat(); + } + + // Create a date from any other input. + else { + $this->constructFallback(); + } + + // Clean up the error messages. + $this->getErrors(); + $this->errors = array_unique($this->errors); + + // Now that we've validated the input, clean up the extra values. + if (empty($settings['debug'])) { + unset( + $this->input_time_raw, + $this->input_time_adjusted, + $this->input_timezone_raw, + $this->input_timezone_adjusted, + $this->input_format_raw, + $this->input_format_adjusted, + $this->validate_format + ); + } + + } + + /** + * Implementation of __toString() for dates. The base DateTime + * class does not implement this. + * + * @see https://bugs.php.net/bug.php?id=62911 and + * http://www.serverphorums.com/read.php?7,555645 + */ + public function __toString() { + $format = self::FORMAT; + return $this->format($format) . ' ' . $this->getTimeZone()->getName(); + } + + /** + * Prepare the input value before trying to use it. + * Can be overridden to handle special cases. + * + * @param mixed $time + * An input value, which could be a timestamp, a string, + * or an array of date parts. + */ + public function prepareTime($time) { + $this->input_time_adjusted = $time; + } + + /** + * Prepare the timezone before trying to use it. + * Most imporantly, make sure we have a valid timezone + * object before moving further. + * + * @param mixed $timezone + * Either a timezone name or a timezone object or NULL. + */ + public function prepareTimezone($timezone) { + // If the passed in timezone is a valid timezone object, use it. + if ($timezone instanceOf \DateTimezone) { + $timezone_adjusted = $timezone; + } + + // When the passed-in time is a DateTime object with its own + // timezone, try to use the date's timezone. + elseif (empty($timezone) && $this->input_time_adjusted instanceOf \DateTime) { + $timezone_adjusted = $this->input_time_adjusted->getTimezone(); + } + + // Allow string timezone input, and create a timezone from it. + elseif (!empty($timezone) && is_string($timezone)) { + $timezone_adjusted = new \DateTimeZone($timezone); + } + + // Default to the system timezone when not explicitly provided. + // If the system timezone is missing, use 'UTC'. + if (empty($timezone_adjusted) || !$timezone_adjusted instanceOf \DateTimezone) { + $system_timezone = date_default_timezone_get(); + $timezone_name = !empty($system_timezone) ? $system_timezone : 'UTC'; + $timezone_adjusted = new \DateTimeZone($timezone_name); + } + + // We are finally certain that we have a usable timezone. + $this->input_timezone_adjusted = $timezone_adjusted; + } + + /** + * Prepare the input format before trying to use it. + * Can be overridden to handle special cases. + * + * @param string $format + * A PHP format string. + */ + public function prepareFormat($format) { + $this->input_format_adjusted = $format; + } + + /** + * Check if input is a DateTime object. + * + * @return boolean + * TRUE if the input time is a DateTime object. + */ + public function inputIsObject() { + return $this->input_time_adjusted instanceOf \DateTime; + } + + /** + * Create a date object from an input date object. + */ + public function constructFromObject() { + try { + $this->input_time_adjusted = $this->input_time_adjusted->format(self::FORMAT); + parent::__construct($this->input_time_adjusted, $this->input_timezone_adjusted); + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Check if input time seems to be a timestamp. + * + * Providing an input format will prevent ISO values without separators + * from being mis-interpreted as timestamps. Providing a format can also + * avoid interpreting a value like '2010' with a format of 'Y' as a + * timestamp. The 'U' format indicates this is a timestamp. + * + * @return boolean + * TRUE if the input time is a timestamp. + */ + public function inputIsTimestamp() { + return is_numeric($this->input_time_adjusted) && (empty($this->input_format_adjusted) || $this->input_format_adjusted == 'U'); + } + + /** + * Create a date object from timestamp input. + * + * The timezone for timestamps is always UTC. In this case the + * timezone we set controls the timezone used when displaying + * the value using format(). + */ + public function constructFromTimestamp() { + try { + parent::__construct('', $this->input_timezone_adjusted); + $this->setTimestamp($this->input_time_adjusted); + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Check if input is an array of date parts. + * + * @return boolean + * TRUE if the input time is a DateTime object. + */ + public function inputIsArray() { + return is_array($this->input_time_adjusted); + } + + /** + * Create a date object from an array of date parts. + * + * Convert the input value into an ISO date, forcing a full ISO + * date even if some values are missing. + */ + public function constructFromArray() { + try { + parent::__construct('', $this->input_timezone_adjusted); + $this->input_time_adjusted = self::prepareArray($this->input_time_adjusted, TRUE); + if (self::checkArray($this->input_time_adjusted)) { + // Even with validation, we can end up with a value that the + // parent class won't handle, like a year outside the range + // of -9999 to 9999, which will pass checkdate() but + // fail to construct a date object. + $this->input_time_adjusted = self::arrayToISO($this->input_time_adjusted); + parent::__construct($this->input_time_adjusted, $this->input_timezone_adjusted); + } + else { + throw new \Exception('The array contains invalid values.'); + } + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Check if input is a string with an expected format. + * + * @return boolean + * TRUE if the input time is a string with an expected format. + */ + public function inputIsFormat() { + return is_string($this->input_time_adjusted) && !empty($this->input_format_adjusted); + } + + /** + * Create a date object from an input format. + * + */ + public function constructFromFormat() { + // Try to create a date from the format and use it if possible. + // A regular try/catch won't work right here, if the value is + // invalid it doesn't return an exception. + try { + parent::__construct('', $this->input_timezone_adjusted); + $date = parent::createFromFormat($this->input_format_adjusted, $this->input_time_adjusted, $this->input_timezone_adjusted); + if (!$date instanceOf \DateTime) { + throw new \Exception('The date cannot be created from a format.'); + } + else { + $this->setTimestamp($date->getTimestamp()); + $this->setTimezone($date->getTimezone()); + + try { + // The createFromFormat function is forgiving, it might + // create a date that is not exactly a match for the provided + // value, so test for that. For instance, an input value of + // '11' using a format of Y (4 digits) gets created as + // '0011' instead of '2011'. + // Use the parent::format() because we do not want to use + // the IntlDateFormatter here. + if ($this->validate_format && parent::format($this->input_format_adjusted) != $this->input_time_raw) { + throw new \Exception('The created date does not match the input value.'); + } + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Fallback construction for values that don't match any of the + * other patterns. + * + * Let the parent dateTime attempt to turn this string into a + * valid date. + */ + public function constructFallback() { + try { + @parent::__construct($this->input_time_adjusted, $this->input_timezone_adjusted); + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Examine getLastErrors() and see what errors to report. + * + * We're interested in two kinds of errors: anything that DateTime + * considers an error, and also a warning that the date was invalid. + * PHP creates a valid date from invalid data with only a warning, + * 2011-02-30 becomes 2011-03-03, for instance, but we don't want that. + * + * @see http://us3.php.net/manual/en/time.getlasterrors.php + */ + public function getErrors() { + $errors = $this->getLastErrors(); + if (!empty($errors['errors'])) { + $this->errors += $errors['errors']; + } + if (!empty($errors['warnings']) && in_array('The parsed date was invalid', $errors['warnings'])) { + $this->errors[] = 'The date is invalid.'; + } + } + + /** + * Detect if there were errors in the processing of this date. + */ + function hasErrors() { + if (count($this->errors)) { + return TRUE; + } + + return FALSE; + } + + /** + * Creates an ISO date from an array of values. + * + * @param array $array + * An array of date values keyed by date part. + * @param bool $force_valid_date + * (optional) Whether to force a full date by filling in missing + * values. Defaults to FALSE. + * + * @return string + * The date as an ISO string. + */ + public static function arrayToISO($array, $force_valid_date = FALSE) { + $array = self::prepareArray($array, $force_valid_date); + $input_time_adjusted = ''; + if ($array['year'] !== '') { + $input_time_adjusted = self::datePad(intval($array['year']), 4); + if ($force_valid_date || $array['month'] !== '') { + $input_time_adjusted .= '-' . self::datePad(intval($array['month'])); + if ($force_valid_date || $array['day'] !== '') { + $input_time_adjusted .= '-' . self::datePad(intval($array['day'])); + } + } + } + if ($array['hour'] !== '') { + $input_time_adjusted .= $input_time_adjusted ? 'T' : ''; + $input_time_adjusted .= self::datePad(intval($array['hour'])); + if ($force_valid_date || $array['minute'] !== '') { + $input_time_adjusted .= ':' . self::datePad(intval($array['minute'])); + if ($force_valid_date || $array['second'] !== '') { + $input_time_adjusted .= ':' . self::datePad(intval($array['second'])); + } + } + } + return $input_time_adjusted; + } + + /** + * Creates a complete array from a possibly incomplete array of date parts. + * + * @param array $array + * An array of date values keyed by date part. + * @param bool $force_valid_date + * (optional) Whether to force a valid date by filling in missing + * values with valid values or just to use empty values instead. + * Defaults to FALSE. + * + * @return array + * A complete array of date parts. + */ + public static function prepareArray($array, $force_valid_date = FALSE) { + if ($force_valid_date) { + $array += array( + 'year' => 0, + 'month' => 1, + 'day' => 1, + 'hour' => 0, + 'minute' => 0, + 'second' => 0, + ); + } + else { + $array += array( + 'year' => '', + 'month' => '', + 'day' => '', + 'hour' => '', + 'minute' => '', + 'second' => '', + ); + } + return $array; + } + + /** + * Check that an array of date parts has a year, month, and day, + * and that those values create a valid date. If time is provided, + * verify that the time values are valid. Sort of an + * equivalent to checkdate(). + * + * @param array $array + * An array of datetime values keyed by date part. + * + * @return boolean + * TRUE if the datetime parts contain valid values, otherwise FALSE. + */ + public static function checkArray($array) { + $valid_date = FALSE; + $valid_input_time_adjusted = TRUE; + // Check for a valid date using checkdate(). Only values that + // meet that test are valid. + if (array_key_exists('year', $array) && array_key_exists('month', $array) && array_key_exists('day', $array)) { + if (@checkdate($array['month'], $array['day'], $array['year'])) { + $valid_date = TRUE; + } + } + // Testing for valid time is reversed. Missing time is OK, + // but incorrect values are not. + foreach (array('hour', 'minute', 'second') as $key) { + if (array_key_exists($key, $array)) { + $value = $array[$key]; + switch ($value) { + case 'hour': + if (!preg_match('/^([1-2][0-3]|[01]?[0-9])$/', $value)) { + $valid_input_time_adjusted = FALSE; + } + break; + case 'minute': + case 'second': + default: + if (!preg_match('/^([0-5][0-9]|[0-9])$/', $value)) { + $valid_input_time_adjusted = FALSE; + } + break; + } + } + } + return $valid_date && $valid_input_time_adjusted; + } + + /** + * Helper function to left pad date parts with zeros. + * + * Provided because this is needed so often with dates. + * + * @param int $value + * The value to pad. + * @param int $size + * (optional) Size expected, usually 2 or 4. Defaults to 2. + * + * @return string + * The padded value. + */ + public static function datePad($value, $size = 2) { + return sprintf("%0" . $size . "d", $value); + } + + + /** + * Test if the IntlDateFormatter is available and we have the + * right information to be able to use it. + */ + function canUseIntl() { + return class_exists('IntlDateFormatter') && !empty($this->calendar) && !empty($this->locale); + } + + /** + * Format the date for display. + * + * Use the IntlDateFormatter to display the format, if possible. + * Because the IntlDateFormatter is not always available, we + * add an optional array of settings that provides the information + * the IntlDateFormatter will need. + * + * @param string $format + * A format string using either PHP's date() or the + * IntlDateFormatter() format. + * @params array $settings + * - string $format_string_type + * Which pattern is used by the format string. When using the + * Intl formatter, the format string must use the Intl pattern, + * which is different from the pattern used by the DateTime + * format function. Defaults to DateTimeBase::PHP. + * - string $locale + * A locale name, using the format specified by the + * intlDateFormatter class. Used to control the result of the + * format() method if that class is available. + * Defaults to the locale set by the constructor. + * - string $timezone + * A timezone name. Defaults to the timezone of the date object. + * - string $calendar + * A calendar to use for the date, Defaults to the calendar + * set by the constructor. + * - int $datetype + * The datetype to use in the formatter, defaults to + * IntlDateFormatter::FULL. + * - int $timetype + * The datetype to use in the formatter, defaults to + * IntlDateFormatter::FULL. + * - boolean $lenient + * Whether or not to use lenient processing in the intl + * formatter. Defaults to FALSE; + * + * @return string + * The formatted value of the date. + * + * @TODO + * Potentially there will be additional ways to take advantage + * of locale and calendar in date handling in the future. + */ + function format($format, $settings = array()) { + + $format_string_type = isset($settings['format_string_type']) ? $settings['format_string_type'] : self::PHP; + $locale = !empty($settings['locale']) ? $settings['locale'] : $this->locale; + $calendar = !empty($settings['calendar']) ? $settings['calendar'] : $this->calendar; + $timezone = !empty($settings['timezone']) ? $settings['timezone'] : $this->getTimezone()->getName(); + $lenient = !empty($settings['lenient']) ? $settings['lenient'] : FALSE; + + // Format the date and catch errors. + try { + + // If we have what we need to use the IntlDateFormatter, do so. + if ($this->canUseIntl() && $format_string_type == self::INTL) { + + $calendar_type = \IntlDateFormatter::GREGORIAN; + + // If we have information about a calendar and it's not already + // in the locale, add it. + if (!empty($calendar) && $calendar != self::CALENDAR && !stristr($locale, '@calendar=')) { + $locale .= '@calendar=' . $calendar; + } + + // If we're working with a non-gregorian calendar, indicate that. + if (stristr($locale, '@calendar=') && !stristr($locale, '@calendar=' . SELF::CALENDAR)) { + $calendar_type = \IntlDateFormatter::TRADITIONAL; + } + + $datetype = !empty($settings['datetype']) ? $settings['datetype'] : \IntlDateFormatter::FULL; + $timetype = !empty($settings['timetype']) ? $settings['timetype'] : \IntlDateFormatter::FULL; + $formatter = new \IntlDateFormatter($locale, $datetype, $timetype, $timezone, $calendar_type); + $formatter->setLenient($lenient); + $value = $formatter->format($format); + } + + // Otherwise, use the parent method. + else { + $value = parent::format($format); + } + } + catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + return $value; + } +} diff --git a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php new file mode 100644 index 0000000..e1ed41c --- /dev/null +++ b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php @@ -0,0 +1,169 @@ + 2014, 'month => 4). + * Defaults to 'now'. + * @param mixed $timezone + * PHP DateTimeZone object, string or NULL allowed. + * Defaults to NULL. + * @param string $format + * PHP date() type format for parsing the input. This is recommended + * to use things like negative years, which php's parser fails on, or + * any other specialized input with a known format. If provided the + * date will be created using the createFromFormat() method. + * Defaults to NULL. + * @see http://us3.php.net/manual/en/datetime.createfromformat.php + * @params array $settings + * - boolean $validate_format + * The format used in createFromFormat() allows slightly different + * values than format(). If we use an input format that works in + * both functions we can add a validation step to confirm that the + * date created from a format string exactly matches the input. + * We need to know if this can be relied on to do that validation. + * Defaults to TRUE. + * - string $locale + * A locale name, using the pattern specified by the + * intlDateFormatter class. Used to control the result of the + * format() method if that class is available. Defaults to NULL. + * - string $calendar + * A calendar to use for the date, Defaults to NULL. + * + * @return object + * A DateTime object set to the requested date and timezone. + * + * @TODO + * Potentially there will be additional ways to take advantage + * of locale and calendar in date handling in the future. + */ + public function __construct($time = 'now', $timezone = NULL, $format = NULL, $settings = array()) { + + // Instantiate the parent class. + parent::__construct($time, $timezone, $format, $settings); + + // Attempt to translate the error messages. + foreach ($this->errors as &$error) { + $error = t($error); + } + } + + /** + * Format the date for display. + * + * Use the IntlDateFormatter to display the format, if available. + * Because the IntlDateFormatter is not always available, we + * need to know whether the $format string uses the standard + * format strings used by the date() function or the alternative + * format provided by the IntlDateFormatter. + * + * @param string $format + * A format string using either date() or IntlDateFormatter() + * format. + * @params array $settings + * - string $format_string_type + * Which pattern is used by the format string. When using the + * Intl formatter, the format string must use the Intl pattern, + * which is different from the pattern used by the DateTime + * format function. Defaults to DateTimeBase::PHP. + * - string $locale + * A locale name, using the format specified by the + * intlDateFormatter class. Used to control the result of the + * format() method if that class is available. + * Defaults to the locale set by the constructor. + * - string $timezone + * A timezone name. Defaults to the timezone of the date object. + * - string $calendar + * A calendar to use for the date, Defaults to the calendar + * set by the constructor. + * - int $datetype + * The datetype to use in the formatter, defaults to + * IntlDateFormatter::FULL. + * - int $timetype + * The datetype to use in the formatter, defaults to + * IntlDateFormatter::FULL. + * - boolean $lenient + * Whether or not to use lenient processing in the intl + * formatter. Defaults to FALSE; + * - string $langcode + * The Drupal langcode to use when formatting the output of this + * date. If NULL, Defaults to the language used to display the page. + * + * @return string + * The formatted value of the date. + * + * @TODO + * Potentially there will be additional ways to take advantage + * of locale and calendar in date handling in the future. + */ + function format($format, $settings = array()) { + + $format_string_type = isset($settings['format_string_type']) ? $settings['format_string_type'] : self::PHP; + + // We can set the langcode and locale using Drupal values. + $this->langcode = !empty($settings['langcode']) ? $settings['langcode'] : language(LANGUAGE_TYPE_INTERFACE)->langcode; + if (empty($settings['locale'])) { + $settings['locale'] = $this->langcode . '-' . variable_get('site_default_country'); + } + + // Format the date and catch errors. + try { + + // If we have what we need to use the IntlDateFormatter, do so. + if ($this->canUseIntl() && $format_string_type == parent::INTL) { + $value = parent::format($format, $settings); + } + + // Otherwise, use the default Drupal method. + else { + + // Encode markers that should be translated. 'A' becomes + // '\xEF\AA\xFF'. xEF and xFF are invalid UTF-8 sequences, + // and we assume they are not in the input string. + // Paired backslashes are isolated to prevent errors in + // read-ahead evaluation. The read-ahead expression ensures that + // A matches, but not \A. + $format = preg_replace(array('/\\\\\\\\/', '/(?langcode); + + // Translate the marked sequences. + $value = preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format); + } + } + catch (\Exception $e) { + $this->errors[] = t($e->getMessage()); + } + return $value; + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Date.php b/core/lib/Drupal/Core/TypedData/Type/Date.php index 07767e9..ae54856 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Date.php +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -7,16 +7,17 @@ namespace Drupal\Core\TypedData\Type; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\TypedData\TypedDataInterface; -use DateTime; use InvalidArgumentException; /** * The date data type. * - * The plain value of a date is an instance of the DateTime class. For setting - * the value an instance of the DateTime class, any string supported by - * DateTime::__construct(), or a timestamp as integer may be passed. + * The plain value of a date is an instance of the DrupalDateTime class. For setting + * the value any value supported by the __construct() of the DrupalDateTime + * class will work, including a DateTime object, a timestamp, a string + * date, or an array of date parts. */ class Date extends TypedData implements TypedDataInterface { @@ -31,18 +32,17 @@ class Date extends TypedData implements TypedDataInterface { * Implements TypedDataInterface::setValue(). */ public function setValue($value) { - if ($value instanceof DateTime || !isset($value)) { + + // Don't try to create a date from an empty value. + // It would default to the current time. + if (!isset($value)) { $this->value = $value; } - // Treat integer values as timestamps, even if supplied as PHP string. - elseif ((string) (int) $value === (string) $value) { - $this->value = new DateTime('@' . $value); - } - elseif (is_string($value)) { - $this->value = new DateTime($value); - } else { - throw new InvalidArgumentException("Invalid date format given."); + $this->value = $value instanceOf DrupalDateTime ? $value : new DrupalDateTime($value); + if ($this->value->hasErrors()) { + throw new InvalidArgumentException("Invalid date format given."); + } } } @@ -50,7 +50,7 @@ public function setValue($value) { * Implements TypedDataInterface::getString(). */ public function getString() { - return (string) $this->getValue()->format(DateTime::ISO8601); + return (string) $this->getValue()->__toString(); } /** diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php index cead0cd..3b9fa9b 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -7,6 +7,7 @@ namespace Drupal\comment; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityFormController; @@ -236,7 +237,8 @@ public function validate(array $form, array &$form_state) { $account = user_load_by_name($form_state['values']['name']); $form_state['values']['uid'] = $account ? $account->uid : 0; - if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) { + $date = new DrupalDateTime($form_state['values']['date']); + if ($date->hasErrors()) { form_set_error('date', t('You have to specify a valid date.')); } if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) { @@ -271,7 +273,8 @@ public function submit(array $form, array &$form_state) { if (empty($comment->date)) { $comment->date = 'now'; } - $comment->created = strtotime($comment->date); + $date = new DrupalDateTime($comment->date); + $comment->created = $date->getTimestamp(); $comment->changed = REQUEST_TIME; // If the comment was posted by a registered user, assign the author's ID. diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index b9bf7eb..d9545ff 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -7,6 +7,7 @@ namespace Drupal\node; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityFormController; @@ -287,7 +288,8 @@ public function validate(array $form, array &$form_state) { } // Validate the "authored on" field. - if (!empty($node->date) && strtotime($node->date) === FALSE) { + $date = new DrupalDateTime($node->date); + if ($date->hasErrors()) { form_set_error('date', t('You have to specify a valid date.')); } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 2dd1b57..59d9019 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -14,6 +14,7 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Database\Query\SelectExtender; use Drupal\Core\Database\Query\SelectInterface; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Template\Attribute; use Drupal\node\Node; use Drupal\file\File; @@ -1107,7 +1108,8 @@ function node_submit($node) { $node->revision_uid = $user->uid; } - $node->created = !empty($node->date) ? strtotime($node->date) : REQUEST_TIME; + $node_created = new DrupalDateTime(!empty($node->date) ? $node->date : REQUEST_TIME); + $node->created = $node_created->getTimestamp(); $node->validated = TRUE; return $node; diff --git a/core/modules/system/lib/Drupal/system/Tests/Datetime/DateTimeBaseTest.php b/core/modules/system/lib/Drupal/system/Tests/Datetime/DateTimeBaseTest.php new file mode 100644 index 0000000..f7625cd --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Datetime/DateTimeBaseTest.php @@ -0,0 +1,348 @@ + t('Date Object'), + 'description' => t('Test Date Object functionality.') , + 'group' => t('Datetime'), + ); + } + + /** + * Set up required modules. + */ + public static $modules = array(); + + /** + * Test setup. + */ + public function setUp() { + parent::setUp(); + variable_set('date_first_day', 1); + } + + /** + * Test creating dates from string input. + */ + public function testDateStrings() { + + // Create date object from datetime string. + $input = '2009-03-07 10:30'; + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2009-03-07T10:30:00-06:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + + // Same during daylight savings time. + $input = '2009-06-07 10:30'; + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2009-06-07T10:30:00-05:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + + // Create date object from date string. + $input = '2009-03-07'; + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2009-03-07T00:00:00-06:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + + // Same during daylight savings time. + $input = '2009-06-07'; + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2009-06-07T00:00:00-05:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + } + + /** + * Test creating dates from arrays of date parts. + */ + function testDateArrays() { + + // Create date object from date array, date only. + $input = array('year' => 2010, 'month' => 2, 'day' => 28); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2010-02-28T00:00:00-06:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase(array('year' => 2010, 'month' => 2, 'day' => 28), $timezone): should be $expected, found $value."); + + // Create date object from date array with hour. + $input = array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '2010-02-28T10:00:00-06:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase(array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10), $timezone): should be $expected, found $value."); + + } + + /** + * Test creating dates from timestamps. + */ + function testDateTimestamp() { + + // Create date object from a unix timestamp and display it in + // local time. + $input = 0; + $timezone = 'UTC'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '1970-01-01T00:00:00+00:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + + $expected = 'UTC'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone is $value: should be $expected."); + $expected = 0; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset is $value: should be $expected."); + + $timezone = 'America/Los_Angeles'; + $date->setTimezone(new DateTimeZone($timezone)); + $value = $date->format('c'); + $expected = '1969-12-31T16:00:00-08:00'; + $this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value."); + + $expected = 'America/Los_Angeles'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '-28800'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + + // Create a date using the timestamp of zero, then display its + // value both in UTC and the local timezone. + $input = 0; + $timezone = 'America/Los_Angeles'; + $date = new DateTimeBase($input, $timezone); + $offset = $date->getOffset(); + $value = $date->format('c'); + $expected = '1969-12-31T16:00:00-08:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone): should be $expected, found $value."); + + $expected = 'America/Los_Angeles'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '-28800'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + + $timezone = 'UTC'; + $date->setTimezone(new DateTimeZone($timezone)); + $value = $date->format('c'); + $expected = '1970-01-01T00:00:00+00:00'; + $this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value."); + + $expected = 'UTC'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '0'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + } + + /** + * Test timezone manipulation. + */ + function testTimezoneConversion() { + + // Create date object from datetime string in UTC, and convert + // it to a local date. + $input = '1970-01-01 00:00:00'; + $timezone = 'UTC'; + $date = new DateTimeBase($input, $timezone); + $value = $date->format('c'); + $expected = '1970-01-01T00:00:00+00:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase('$input', '$timezone'): should be $expected, found $value."); + + $expected = 'UTC'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone is $value: should be $expected."); + $expected = 0; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset is $value: should be $expected."); + + $timezone = 'America/Los_Angeles'; + $date->setTimezone(new DateTimeZone($timezone)); + $value = $date->format('c'); + $expected = '1969-12-31T16:00:00-08:00'; + $this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value."); + + $expected = 'America/Los_Angeles'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '-28800'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + + // Convert the local time to UTC using string input. + $input = '1969-12-31 16:00:00'; + $timezone = 'America/Los_Angeles'; + $date = new DateTimeBase($input, $timezone); + $offset = $date->getOffset(); + $value = $date->format('c'); + $expected = '1969-12-31T16:00:00-08:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase('$input', '$timezone'): should be $expected, found $value."); + + $expected = 'America/Los_Angeles'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '-28800'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + + $timezone = 'UTC'; + $date->setTimezone(new DateTimeZone($timezone)); + $value = $date->format('c'); + $expected = '1970-01-01T00:00:00+00:00'; + $this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value."); + + $expected = 'UTC'; + $value = $date->getTimeZone()->getName(); + $this->assertEqual($expected, $value, "The current timezone should be $expected, found $value."); + $expected = '0'; + $value = $date->getOffset(); + $this->assertEqual($expected, $value, "The current offset should be $expected, found $value."); + + } + + /** + * Test creating dates from format strings. + */ + function testDateFormat() { + + // Create a year-only date. + $input = '2009'; + $timezone = NULL; + $format = 'Y'; + $date = new DateTimeBase($input, $timezone, $format); + $value = $date->format('Y'); + $expected = '2009'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone, $format): should be $expected, found $value."); + + // Create a month and year-only date. + $input = '2009-10'; + $timezone = NULL; + $format = 'Y-m'; + $date = new DateTimeBase($input, $timezone, $format); + $value = $date->format('Y-m'); + $expected = '2009-10'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone, $format): should be $expected, found $value."); + + // Create a time-only date. + $input = '0000-00-00T10:30:00'; + $timezone = NULL; + $format = 'Y-m-d\TH:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $value = $date->format('H:i:s'); + $expected = '10:30:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone, $format): should be $expected, found $value."); + + // Create a time-only date. + $input = '10:30:00'; + $timezone = NULL; + $format = 'H:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $value = $date->format('H:i:s'); + $expected = '10:30:00'; + $this->assertEqual($expected, $value, "Test new DateTimeBase($input, $timezone, $format): should be $expected, found $value."); + + } + + /** + * Test invalid date handling. + */ + function testInvalidDates() { + + // Test for invalid month names when we are using a short version + // of the month + $input = '23 abc 2012'; + $timezone = NULL; + $format = 'd M Y'; + $date = new DateTimeBase($input, $timezone, $format); + $this->assertNotEqual(count($date->errors), 0, "$input contains an invalid month name and produces errors."); + + // Test for invalid hour. + $input = '0000-00-00T45:30:00'; + $timezone = NULL; + $format = 'Y-m-d\TH:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $this->assertNotEqual(count($date->errors), 0, "$input contains an invalid hour and produces errors."); + + // Test for invalid day. + $input = '0000-00-99T05:30:00'; + $timezone = NULL; + $format = 'Y-m-d\TH:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $this->assertNotEqual(count($date->errors), 0, "$input contains an invalid day and produces errors."); + + // Test for invalid month. + $input = '0000-75-00T15:30:00'; + $timezone = NULL; + $format = 'Y-m-d\TH:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $this->assertNotEqual(count($date->errors), 0, "$input contains an invalid month and produces errors."); + + // Test for invalid year. + $input = '11-08-01T15:30:00'; + $timezone = NULL; + $format = 'Y-m-d\TH:i:s'; + $date = new DateTimeBase($input, $timezone, $format); + $this->assertNotEqual(count($date->errors), 0, "$input contains an invalid year and produces errors."); + + // Test for invalid year from date array. 10000 as a year will + // create an exception error in the PHP DateTime object. + $input = array('year' => 10000, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $this->assertNotEqual(count($date->errors), 0, "array('year' => 10000, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0) contains an invalid year and produces errors."); + + // Test for invalid month from date array. + $input = array('year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $this->assertNotEqual(count($date->errors), 0, "array('year' => 2010, 'month' => 27, 'day' => 8, 'hour' => 8, 'minute' => 0, 'second' => 0) contains an invalid month and produces errors."); + + // Test for invalid hour from date array. + $input = array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $this->assertNotEqual(count($date->errors), 0, "array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 80, 'minute' => 0, 'second' => 0) contains an invalid hour and produces errors."); + + // Test for invalid minute from date array. + $input = array('year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0); + $timezone = 'America/Chicago'; + $date = new DateTimeBase($input, $timezone); + $this->assertNotEqual(count($date->errors), 0, "array('year' => 2010, 'month' => 7, 'day' => 8, 'hour' => 8, 'minute' => 88, 'second' => 0) contains an invalid minute and produces errors."); + + } + + /** + * Tear down after tests. + */ + public function tearDown() { + variable_del('date_first_day'); + parent::tearDown(); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php index 62bd35e..00e4c95 100644 --- a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php @@ -8,7 +8,7 @@ namespace Drupal\system\Tests\TypedData; use Drupal\simpletest\WebTestBase; -use DateTime; +use Drupal\Core\Datetime\DrupalDateTime; use DateInterval; /** @@ -72,7 +72,7 @@ public function testGetAndSet() { $this->assertNull($wrapper->getValue(), 'Float wrapper is null-able.'); // Date type. - $value = new DateTime('@' . REQUEST_TIME); + $value = new DrupalDateTime(REQUEST_TIME); $wrapper = $this->createTypedData(array('type' => 'date'), $value); $this->assertTrue($wrapper->getValue() === $value, 'Date value was fetched.'); $new_value = REQUEST_TIME + 1;