Added another front page space for Yaffs info. Added roave security for composer.
[yaffs-website] / web / core / lib / Drupal / Component / Datetime / DateTimePlus.php
1 <?php
2
3 namespace Drupal\Component\Datetime;
4 use Drupal\Component\Utility\ToStringTrait;
5
6 /**
7  * Wraps DateTime().
8  *
9  * This class wraps the PHP DateTime class with more flexible initialization
10  * parameters, allowing a date to be created from an existing date object,
11  * a timestamp, a string with an unknown format, a string with a known
12  * format, or an array of date parts. It also adds an errors array
13  * and a __toString() method to the date object.
14  *
15  * This class is less lenient than the DateTime class. It changes
16  * the default behavior for handling date values like '2011-00-00'.
17  * The DateTime class would convert that value to '2010-11-30' and report
18  * a warning but not an error. This extension treats that as an error.
19  *
20  * As with the DateTime class, a date object may be created even if it has
21  * errors. It has an errors array attached to it that explains what the
22  * errors are. This is less disruptive than allowing datetime exceptions
23  * to abort processing. The calling script can decide what to do about
24  * errors using hasErrors() and getErrors().
25  */
26 class DateTimePlus {
27
28   use ToStringTrait;
29
30   const FORMAT   = 'Y-m-d H:i:s';
31
32   /**
33    * A RFC7231 Compliant date.
34    *
35    * http://tools.ietf.org/html/rfc7231#section-7.1.1.1
36    *
37    * Example: Sun, 06 Nov 1994 08:49:37 GMT
38    */
39   const RFC7231 = 'D, d M Y H:i:s \G\M\T';
40
41   /**
42    * An array of possible date parts.
43    */
44   protected static $dateParts = [
45     'year',
46     'month',
47     'day',
48     'hour',
49     'minute',
50     'second',
51   ];
52
53   /**
54    * The value of the time value passed to the constructor.
55    */
56   protected $inputTimeRaw = '';
57
58   /**
59    * The prepared time, without timezone, for this date.
60    */
61   protected $inputTimeAdjusted = '';
62
63   /**
64    * The value of the timezone passed to the constructor.
65    */
66   protected $inputTimeZoneRaw = '';
67
68   /**
69    * The prepared timezone object used to construct this date.
70    */
71   protected $inputTimeZoneAdjusted = '';
72
73   /**
74    * The value of the format passed to the constructor.
75    */
76   protected $inputFormatRaw = '';
77
78   /**
79    * The prepared format, if provided.
80    */
81   protected $inputFormatAdjusted = '';
82
83   /**
84    * The value of the language code passed to the constructor.
85    */
86   protected $langcode = NULL;
87
88   /**
89    * An array of errors encountered when creating this date.
90    */
91   protected $errors = [];
92
93   /**
94    * The DateTime object.
95    *
96    * @var \DateTime
97    */
98   protected $dateTimeObject = NULL;
99
100   /**
101    * Creates a date object from an input date object.
102    *
103    * @param \DateTime $datetime
104    *   A DateTime object.
105    * @param array $settings
106    *   @see __construct()
107    *
108    * @return static
109    *   A new DateTimePlus object.
110    */
111   public static function createFromDateTime(\DateTime $datetime, $settings = []) {
112     return new static($datetime->format(static::FORMAT), $datetime->getTimezone(), $settings);
113   }
114
115   /**
116    * Creates a date object from an array of date parts.
117    *
118    * Converts the input value into an ISO date, forcing a full ISO
119    * date even if some values are missing.
120    *
121    * @param array $date_parts
122    *   An array of date parts, like ('year' => 2014, 'month' => 4).
123    * @param mixed $timezone
124    *   (optional) \DateTimeZone object, time zone string or NULL. NULL uses the
125    *   default system time zone. Defaults to NULL.
126    * @param array $settings
127    *   (optional) A keyed array for settings, suitable for passing on to
128    *   __construct().
129    *
130    * @return static
131    *   A new DateTimePlus object.
132    *
133    * @throws \InvalidArgumentException
134    *   If the array date values or value combination is not correct.
135    */
136   public static function createFromArray(array $date_parts, $timezone = NULL, $settings = []) {
137     $date_parts = static::prepareArray($date_parts, TRUE);
138     if (static::checkArray($date_parts)) {
139       // Even with validation, we can end up with a value that the
140       // DateTime class won't handle, like a year outside the range
141       // of -9999 to 9999, which will pass checkdate() but
142       // fail to construct a date object.
143       $iso_date = static::arrayToISO($date_parts);
144       return new static($iso_date, $timezone, $settings);
145     }
146     else {
147       throw new \InvalidArgumentException('The array contains invalid values.');
148     }
149   }
150
151   /**
152    * Creates a date object from timestamp input.
153    *
154    * The timezone of a timestamp is always UTC. The timezone for a
155    * timestamp indicates the timezone used by the format() method.
156    *
157    * @param int $timestamp
158    *   A UNIX timestamp.
159    * @param mixed $timezone
160    *   @see __construct()
161    * @param array $settings
162    *   @see __construct()
163    *
164    * @return static
165    *   A new DateTimePlus object.
166    *
167    * @throws \InvalidArgumentException
168    *   If the timestamp is not numeric.
169    */
170   public static function createFromTimestamp($timestamp, $timezone = NULL, $settings = []) {
171     if (!is_numeric($timestamp)) {
172       throw new \InvalidArgumentException('The timestamp must be numeric.');
173     }
174     $datetime = new static('', $timezone, $settings);
175     $datetime->setTimestamp($timestamp);
176     return $datetime;
177   }
178
179   /**
180    * Creates a date object from an input format.
181    *
182    * @param string $format
183    *   PHP date() type format for parsing the input. This is recommended
184    *   to use things like negative years, which php's parser fails on, or
185    *   any other specialized input with a known format. If provided the
186    *   date will be created using the createFromFormat() method.
187    *   @see http://php.net/manual/datetime.createfromformat.php
188    * @param mixed $time
189    *   @see __construct()
190    * @param mixed $timezone
191    *   @see __construct()
192    * @param array $settings
193    *   - validate_format: (optional) Boolean choice to validate the
194    *     created date using the input format. The format used in
195    *     createFromFormat() allows slightly different values than format().
196    *     Using an input format that works in both functions makes it
197    *     possible to a validation step to confirm that the date created
198    *     from a format string exactly matches the input. This option
199    *     indicates the format can be used for validation. Defaults to TRUE.
200    *   @see __construct()
201    *
202    * @return static
203    *   A new DateTimePlus object.
204    *
205    * @throws \InvalidArgumentException
206    *   If the a date cannot be created from the given format.
207    * @throws \UnexpectedValueException
208    *   If the created date does not match the input value.
209    */
210   public static function createFromFormat($format, $time, $timezone = NULL, $settings = []) {
211     if (!isset($settings['validate_format'])) {
212       $settings['validate_format'] = TRUE;
213     }
214
215     // Tries to create a date from the format and use it if possible.
216     // A regular try/catch won't work right here, if the value is
217     // invalid it doesn't return an exception.
218     $datetimeplus = new static('', $timezone, $settings);
219
220     $date = \DateTime::createFromFormat($format, $time, $datetimeplus->getTimezone());
221     if (!$date instanceof \DateTime) {
222       throw new \InvalidArgumentException('The date cannot be created from a format.');
223     }
224     else {
225       // Functions that parse date is forgiving, it might create a date that
226       // is not exactly a match for the provided value, so test for that by
227       // re-creating the date/time formatted string and comparing it to the input. For
228       // instance, an input value of '11' using a format of Y (4 digits) gets
229       // created as '0011' instead of '2011'.
230       if ($date instanceof DateTimePlus) {
231         $test_time = $date->format($format, $settings);
232       }
233       elseif ($date instanceof \DateTime) {
234         $test_time = $date->format($format);
235       }
236       $datetimeplus->setTimestamp($date->getTimestamp());
237       $datetimeplus->setTimezone($date->getTimezone());
238
239       if ($settings['validate_format'] && $test_time != $time) {
240         throw new \UnexpectedValueException('The created date does not match the input value.');
241       }
242     }
243     return $datetimeplus;
244   }
245
246   /**
247    * Constructs a date object set to a requested date and timezone.
248    *
249    * @param string $time
250    *   (optional) A date/time string. Defaults to 'now'.
251    * @param mixed $timezone
252    *   (optional) \DateTimeZone object, time zone string or NULL. NULL uses the
253    *   default system time zone. Defaults to NULL.
254    * @param array $settings
255    *   (optional) Keyed array of settings. Defaults to empty array.
256    *   - langcode: (optional) String two letter language code used to control
257    *     the result of the format(). Defaults to NULL.
258    *   - debug: (optional) Boolean choice to leave debug values in the
259    *     date object for debugging purposes. Defaults to FALSE.
260    */
261   public function __construct($time = 'now', $timezone = NULL, $settings = []) {
262
263     // Unpack settings.
264     $this->langcode = !empty($settings['langcode']) ? $settings['langcode'] : NULL;
265
266     // Massage the input values as necessary.
267     $prepared_time = $this->prepareTime($time);
268     $prepared_timezone = $this->prepareTimezone($timezone);
269
270     try {
271       $this->errors = [];
272       if (!empty($prepared_time)) {
273         $test = date_parse($prepared_time);
274         if (!empty($test['errors'])) {
275           $this->errors = $test['errors'];
276         }
277       }
278
279       if (empty($this->errors)) {
280         $this->dateTimeObject = new \DateTime($prepared_time, $prepared_timezone);
281       }
282     }
283     catch (\Exception $e) {
284       $this->errors[] = $e->getMessage();
285     }
286
287     // Clean up the error messages.
288     $this->checkErrors();
289   }
290
291   /**
292    * Renders the timezone name.
293    *
294    * @return string
295    */
296   public function render() {
297     return $this->format(static::FORMAT) . ' ' . $this->getTimeZone()->getName();
298   }
299
300   /**
301    * Implements the magic __call method.
302    *
303    * Passes through all unknown calls onto the DateTime object.
304    */
305   public function __call($method, $args) {
306     // @todo consider using assert() as per https://www.drupal.org/node/2451793.
307     if (!isset($this->dateTimeObject)) {
308       throw new \Exception('DateTime object not set.');
309     }
310     if (!method_exists($this->dateTimeObject, $method)) {
311       throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $method));
312     }
313     return call_user_func_array([$this->dateTimeObject, $method], $args);
314   }
315
316   /**
317    * Returns the difference between two DateTimePlus objects.
318    *
319    * @param \Drupal\Component\Datetime\DateTimePlus|\DateTime $datetime2
320    *    The date to compare to.
321    * @param bool $absolute
322    *    Should the interval be forced to be positive?
323    *
324    * @return \DateInterval
325    *    A DateInterval object representing the difference between the two dates.
326    *
327    * @throws \BadMethodCallException
328    *    If the input isn't a DateTime or DateTimePlus object.
329    */
330   public function diff($datetime2, $absolute = FALSE) {
331     if ($datetime2 instanceof DateTimePlus) {
332       $datetime2 = $datetime2->dateTimeObject;
333     }
334     if (!($datetime2 instanceof \DateTime)) {
335       throw new \BadMethodCallException(sprintf('Method %s expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object', __METHOD__));
336     }
337     return $this->dateTimeObject->diff($datetime2, $absolute);
338   }
339
340   /**
341    * Implements the magic __callStatic method.
342    *
343    * Passes through all unknown static calls onto the DateTime object.
344    */
345   public static function __callStatic($method, $args) {
346     if (!method_exists('\DateTime', $method)) {
347       throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_called_class(), $method));
348     }
349     return call_user_func_array(['\DateTime', $method], $args);
350   }
351
352   /**
353    * Implements the magic __clone method.
354    *
355    * Deep-clones the DateTime object we're wrapping.
356    */
357   public function __clone() {
358     $this->dateTimeObject = clone($this->dateTimeObject);
359   }
360
361   /**
362    * Prepares the input time value.
363    *
364    * Changes the input value before trying to use it, if necessary.
365    * Can be overridden to handle special cases.
366    *
367    * @param mixed $time
368    *   An input value, which could be a timestamp, a string,
369    *   or an array of date parts.
370    *
371    * @return mixed
372    *   The massaged time.
373    */
374   protected function prepareTime($time) {
375     return $time;
376   }
377
378   /**
379    * Prepares the input timezone value.
380    *
381    * Changes the timezone before trying to use it, if necessary.
382    * Most importantly, makes sure there is a valid timezone
383    * object before moving further.
384    *
385    * @param mixed $timezone
386    *   Either a timezone name or a timezone object or NULL.
387    *
388    * @return \DateTimeZone
389    *   The massaged time zone.
390    */
391   protected function prepareTimezone($timezone) {
392     // If the input timezone is a valid timezone object, use it.
393     if ($timezone instanceof \DateTimezone) {
394       $timezone_adjusted = $timezone;
395     }
396
397     // Allow string timezone input, and create a timezone from it.
398     elseif (!empty($timezone) && is_string($timezone)) {
399       $timezone_adjusted = new \DateTimeZone($timezone);
400     }
401
402     // Default to the system timezone when not explicitly provided.
403     // If the system timezone is missing, use 'UTC'.
404     if (empty($timezone_adjusted) || !$timezone_adjusted instanceof \DateTimezone) {
405       $system_timezone = date_default_timezone_get();
406       $timezone_name = !empty($system_timezone) ? $system_timezone : 'UTC';
407       $timezone_adjusted = new \DateTimeZone($timezone_name);
408     }
409
410     // We are finally certain that we have a usable timezone.
411     return $timezone_adjusted;
412   }
413
414   /**
415    * Prepares the input format value.
416    *
417    * Changes the input format before trying to use it, if necessary.
418    * Can be overridden to handle special cases.
419    *
420    * @param string $format
421    *   A PHP format string.
422    *
423    * @return string
424    *   The massaged PHP format string.
425    */
426   protected function prepareFormat($format) {
427     return $format;
428   }
429
430
431
432   /**
433    * Examines getLastErrors() to see what errors to report.
434    *
435    * Two kinds of errors are important: anything that DateTime
436    * considers an error, and also a warning that the date was invalid.
437    * PHP creates a valid date from invalid data with only a warning,
438    * 2011-02-30 becomes 2011-03-03, for instance, but we don't want that.
439    *
440    * @see http://php.net/manual/time.getlasterrors.php
441    */
442   public function checkErrors() {
443     $errors = \DateTime::getLastErrors();
444     if (!empty($errors['errors'])) {
445       $this->errors = array_merge($this->errors, $errors['errors']);
446     }
447     // Most warnings are messages that the date could not be parsed
448     // which causes it to be altered. For validation purposes, a warning
449     // as bad as an error, because it means the constructed date does
450     // not match the input value.
451     if (!empty($errors['warnings'])) {
452       $this->errors[] = 'The date is invalid.';
453     }
454
455     $this->errors = array_values(array_unique($this->errors));
456   }
457
458   /**
459    * Detects if there were errors in the processing of this date.
460    *
461    * @return bool
462    *   TRUE if there were errors in the processing of this date, FALSE
463    *   otherwise.
464    */
465   public function hasErrors() {
466     return (boolean) count($this->errors);
467   }
468
469   /**
470    * Gets error messages.
471    *
472    * Public function to return the error messages.
473    *
474    * @return array
475    *   An array of errors encountered when creating this date.
476    */
477   public function getErrors() {
478     return $this->errors;
479   }
480
481   /**
482    * Creates an ISO date from an array of values.
483    *
484    * @param array $array
485    *   An array of date values keyed by date part.
486    * @param bool $force_valid_date
487    *   (optional) Whether to force a full date by filling in missing
488    *   values. Defaults to FALSE.
489    *
490    * @return string
491    *   The date as an ISO string.
492    */
493   public static function arrayToISO($array, $force_valid_date = FALSE) {
494     $array = static::prepareArray($array, $force_valid_date);
495     $input_time = '';
496     if ($array['year'] !== '') {
497       $input_time = static::datePad(intval($array['year']), 4);
498       if ($force_valid_date || $array['month'] !== '') {
499         $input_time .= '-' . static::datePad(intval($array['month']));
500         if ($force_valid_date || $array['day'] !== '') {
501           $input_time .= '-' . static::datePad(intval($array['day']));
502         }
503       }
504     }
505     if ($array['hour'] !== '') {
506       $input_time .= $input_time ? 'T' : '';
507       $input_time .= static::datePad(intval($array['hour']));
508       if ($force_valid_date || $array['minute'] !== '') {
509         $input_time .= ':' . static::datePad(intval($array['minute']));
510         if ($force_valid_date || $array['second'] !== '') {
511           $input_time .= ':' . static::datePad(intval($array['second']));
512         }
513       }
514     }
515     return $input_time;
516   }
517
518   /**
519    * Creates a complete array from a possibly incomplete array of date parts.
520    *
521    * @param array $array
522    *   An array of date values keyed by date part.
523    * @param bool $force_valid_date
524    *   (optional) Whether to force a valid date by filling in missing
525    *   values with valid values or just to use empty values instead.
526    *   Defaults to FALSE.
527    *
528    * @return array
529    *   A complete array of date parts.
530    */
531   public static function prepareArray($array, $force_valid_date = FALSE) {
532     if ($force_valid_date) {
533       $now = new \DateTime();
534       $array += [
535         'year'   => $now->format('Y'),
536         'month'  => 1,
537         'day'    => 1,
538         'hour'   => 0,
539         'minute' => 0,
540         'second' => 0,
541       ];
542     }
543     else {
544       $array += [
545         'year'   => '',
546         'month'  => '',
547         'day'    => '',
548         'hour'   => '',
549         'minute' => '',
550         'second' => '',
551       ];
552     }
553     return $array;
554   }
555
556   /**
557    * Checks that arrays of date parts will create a valid date.
558    *
559    * Checks that an array of date parts has a year, month, and day,
560    * and that those values create a valid date. If time is provided,
561    * verifies that the time values are valid. Sort of an
562    * equivalent to checkdate().
563    *
564    * @param array $array
565    *   An array of datetime values keyed by date part.
566    *
567    * @return bool
568    *   TRUE if the datetime parts contain valid values, otherwise FALSE.
569    */
570   public static function checkArray($array) {
571     $valid_date = FALSE;
572     $valid_time = TRUE;
573     // Check for a valid date using checkdate(). Only values that
574     // meet that test are valid.
575     if (array_key_exists('year', $array) && array_key_exists('month', $array) && array_key_exists('day', $array)) {
576       if (@checkdate($array['month'], $array['day'], $array['year'])) {
577         $valid_date = TRUE;
578       }
579     }
580     // Testing for valid time is reversed. Missing time is OK,
581     // but incorrect values are not.
582     foreach (['hour', 'minute', 'second'] as $key) {
583       if (array_key_exists($key, $array)) {
584         $value = $array[$key];
585         switch ($key) {
586           case 'hour':
587             if (!preg_match('/^([1-2][0-3]|[01]?[0-9])$/', $value)) {
588               $valid_time = FALSE;
589             }
590             break;
591           case 'minute':
592           case 'second':
593           default:
594             if (!preg_match('/^([0-5][0-9]|[0-9])$/', $value)) {
595               $valid_time = FALSE;
596             }
597             break;
598         }
599       }
600     }
601     return $valid_date && $valid_time;
602   }
603
604   /**
605    * Pads date parts with zeros.
606    *
607    * Helper function for a task that is often required when working with dates.
608    *
609    * @param int $value
610    *   The value to pad.
611    * @param int $size
612    *   (optional) Size expected, usually 2 or 4. Defaults to 2.
613    *
614    * @return string
615    *   The padded value.
616    */
617   public static function datePad($value, $size = 2) {
618     return sprintf("%0" . $size . "d", $value);
619   }
620
621   /**
622    * Formats the date for display.
623    *
624    * @param string $format
625    *   A format string using either PHP's date().
626    * @param array $settings
627    *   - timezone: (optional) String timezone name. Defaults to the timezone
628    *     of the date object.
629    *
630    * @return string
631    *   The formatted value of the date.
632    */
633   public function format($format, $settings = []) {
634
635     // If there were construction errors, we can't format the date.
636     if ($this->hasErrors()) {
637       return;
638     }
639
640     // Format the date and catch errors.
641     try {
642       // Clone the date/time object so we can change the time zone without
643       // disturbing the value stored in the object.
644       $dateTimeObject = clone $this->dateTimeObject;
645       if (isset($settings['timezone'])) {
646         $dateTimeObject->setTimezone(new \DateTimeZone($settings['timezone']));
647       }
648       $value = $dateTimeObject->format($format);
649     }
650     catch (\Exception $e) {
651       $this->errors[] = $e->getMessage();
652     }
653
654     return $value;
655   }
656
657 }