1<?php 2 3namespace Sabre\VObject\Property\ICalendar; 4 5use DateTimeInterface; 6use DateTimeZone; 7use Sabre\VObject\DateTimeParser; 8use Sabre\VObject\InvalidDataException; 9use Sabre\VObject\Property; 10use Sabre\VObject\TimeZoneUtil; 11 12/** 13 * DateTime property. 14 * 15 * This object represents DATE-TIME values, as defined here: 16 * 17 * http://tools.ietf.org/html/rfc5545#section-3.3.4 18 * 19 * This particular object has a bit of hackish magic that it may also in some 20 * cases represent a DATE value. This is because it's a common usecase to be 21 * able to change a DATE-TIME into a DATE. 22 * 23 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 24 * @author Evert Pot (http://evertpot.com/) 25 * @license http://sabre.io/license/ Modified BSD License 26 */ 27class DateTime extends Property 28{ 29 /** 30 * In case this is a multi-value property. This string will be used as a 31 * delimiter. 32 * 33 * @var string|null 34 */ 35 public $delimiter = ','; 36 37 /** 38 * Sets a multi-valued property. 39 * 40 * You may also specify DateTime objects here. 41 * 42 * @param array $parts 43 */ 44 public function setParts(array $parts) 45 { 46 if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) { 47 $this->setDateTimes($parts); 48 } else { 49 parent::setParts($parts); 50 } 51 } 52 53 /** 54 * Updates the current value. 55 * 56 * This may be either a single, or multiple strings in an array. 57 * 58 * Instead of strings, you may also use DateTime here. 59 * 60 * @param string|array|DateTimeInterface $value 61 */ 62 public function setValue($value) 63 { 64 if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) { 65 $this->setDateTimes($value); 66 } elseif ($value instanceof DateTimeInterface) { 67 $this->setDateTimes([$value]); 68 } else { 69 parent::setValue($value); 70 } 71 } 72 73 /** 74 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 75 * 76 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 77 * not yet done, but parameters are not included. 78 * 79 * @param string $val 80 */ 81 public function setRawMimeDirValue($val) 82 { 83 $this->setValue(explode($this->delimiter, $val)); 84 } 85 86 /** 87 * Returns a raw mime-dir representation of the value. 88 * 89 * @return string 90 */ 91 public function getRawMimeDirValue() 92 { 93 return implode($this->delimiter, $this->getParts()); 94 } 95 96 /** 97 * Returns true if this is a DATE-TIME value, false if it's a DATE. 98 * 99 * @return bool 100 */ 101 public function hasTime() 102 { 103 return 'DATE' !== strtoupper((string) $this['VALUE']); 104 } 105 106 /** 107 * Returns true if this is a floating DATE or DATE-TIME. 108 * 109 * Note that DATE is always floating. 110 */ 111 public function isFloating() 112 { 113 return 114 !$this->hasTime() || 115 ( 116 !isset($this['TZID']) && 117 false === strpos($this->getValue(), 'Z') 118 ); 119 } 120 121 /** 122 * Returns a date-time value. 123 * 124 * Note that if this property contained more than 1 date-time, only the 125 * first will be returned. To get an array with multiple values, call 126 * getDateTimes. 127 * 128 * If no timezone information is known, because it's either an all-day 129 * property or floating time, we will use the DateTimeZone argument to 130 * figure out the exact date. 131 * 132 * @param DateTimeZone $timeZone 133 * 134 * @return \DateTimeImmutable 135 */ 136 public function getDateTime(DateTimeZone $timeZone = null) 137 { 138 $dt = $this->getDateTimes($timeZone); 139 if (!$dt) { 140 return; 141 } 142 143 return $dt[0]; 144 } 145 146 /** 147 * Returns multiple date-time values. 148 * 149 * If no timezone information is known, because it's either an all-day 150 * property or floating time, we will use the DateTimeZone argument to 151 * figure out the exact date. 152 * 153 * @param DateTimeZone $timeZone 154 * 155 * @return \DateTimeImmutable[] 156 * @return \DateTime[] 157 */ 158 public function getDateTimes(DateTimeZone $timeZone = null) 159 { 160 // Does the property have a TZID? 161 $tzid = $this['TZID']; 162 163 if ($tzid) { 164 $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); 165 } 166 167 $dts = []; 168 foreach ($this->getParts() as $part) { 169 $dts[] = DateTimeParser::parse($part, $timeZone); 170 } 171 172 return $dts; 173 } 174 175 /** 176 * Sets the property as a DateTime object. 177 * 178 * @param DateTimeInterface $dt 179 * @param bool isFloating If set to true, timezones will be ignored 180 */ 181 public function setDateTime(DateTimeInterface $dt, $isFloating = false) 182 { 183 $this->setDateTimes([$dt], $isFloating); 184 } 185 186 /** 187 * Sets the property as multiple date-time objects. 188 * 189 * The first value will be used as a reference for the timezones, and all 190 * the otehr values will be adjusted for that timezone 191 * 192 * @param DateTimeInterface[] $dt 193 * @param bool isFloating If set to true, timezones will be ignored 194 */ 195 public function setDateTimes(array $dt, $isFloating = false) 196 { 197 $values = []; 198 199 if ($this->hasTime()) { 200 $tz = null; 201 $isUtc = false; 202 203 foreach ($dt as $d) { 204 if ($isFloating) { 205 $values[] = $d->format('Ymd\\THis'); 206 continue; 207 } 208 if (is_null($tz)) { 209 $tz = $d->getTimeZone(); 210 $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); 211 if (!$isUtc) { 212 $this->offsetSet('TZID', $tz->getName()); 213 } 214 } else { 215 $d = $d->setTimeZone($tz); 216 } 217 218 if ($isUtc) { 219 $values[] = $d->format('Ymd\\THis\\Z'); 220 } else { 221 $values[] = $d->format('Ymd\\THis'); 222 } 223 } 224 if ($isUtc || $isFloating) { 225 $this->offsetUnset('TZID'); 226 } 227 } else { 228 foreach ($dt as $d) { 229 $values[] = $d->format('Ymd'); 230 } 231 $this->offsetUnset('TZID'); 232 } 233 234 $this->value = $values; 235 } 236 237 /** 238 * Returns the type of value. 239 * 240 * This corresponds to the VALUE= parameter. Every property also has a 241 * 'default' valueType. 242 * 243 * @return string 244 */ 245 public function getValueType() 246 { 247 return $this->hasTime() ? 'DATE-TIME' : 'DATE'; 248 } 249 250 /** 251 * Returns the value, in the format it should be encoded for JSON. 252 * 253 * This method must always return an array. 254 * 255 * @return array 256 */ 257 public function getJsonValue() 258 { 259 $dts = $this->getDateTimes(); 260 $hasTime = $this->hasTime(); 261 $isFloating = $this->isFloating(); 262 263 $tz = $dts[0]->getTimeZone(); 264 $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']); 265 266 return array_map( 267 function (DateTimeInterface $dt) use ($hasTime, $isUtc) { 268 if ($hasTime) { 269 return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); 270 } else { 271 return $dt->format('Y-m-d'); 272 } 273 }, 274 $dts 275 ); 276 } 277 278 /** 279 * Sets the json value, as it would appear in a jCard or jCal object. 280 * 281 * The value must always be an array. 282 * 283 * @param array $value 284 */ 285 public function setJsonValue(array $value) 286 { 287 // dates and times in jCal have one difference to dates and times in 288 // iCalendar. In jCal date-parts are separated by dashes, and 289 // time-parts are separated by colons. It makes sense to just remove 290 // those. 291 $this->setValue( 292 array_map( 293 function ($item) { 294 return strtr($item, [':' => '', '-' => '']); 295 }, 296 $value 297 ) 298 ); 299 } 300 301 /** 302 * We need to intercept offsetSet, because it may be used to alter the 303 * VALUE from DATE-TIME to DATE or vice-versa. 304 * 305 * @param string $name 306 * @param mixed $value 307 */ 308 public function offsetSet($name, $value) 309 { 310 parent::offsetSet($name, $value); 311 if ('VALUE' !== strtoupper($name)) { 312 return; 313 } 314 315 // This will ensure that dates are correctly encoded. 316 $this->setDateTimes($this->getDateTimes()); 317 } 318 319 /** 320 * Validates the node for correctness. 321 * 322 * The following options are supported: 323 * Node::REPAIR - May attempt to automatically repair the problem. 324 * 325 * This method returns an array with detected problems. 326 * Every element has the following properties: 327 * 328 * * level - problem level. 329 * * message - A human-readable string describing the issue. 330 * * node - A reference to the problematic node. 331 * 332 * The level means: 333 * 1 - The issue was repaired (only happens if REPAIR was turned on) 334 * 2 - An inconsequential issue 335 * 3 - A severe issue. 336 * 337 * @param int $options 338 * 339 * @return array 340 */ 341 public function validate($options = 0) 342 { 343 $messages = parent::validate($options); 344 $valueType = $this->getValueType(); 345 $values = $this->getParts(); 346 try { 347 foreach ($values as $value) { 348 switch ($valueType) { 349 case 'DATE': 350 DateTimeParser::parseDate($value); 351 break; 352 case 'DATE-TIME': 353 DateTimeParser::parseDateTime($value); 354 break; 355 } 356 } 357 } catch (InvalidDataException $e) { 358 $messages[] = [ 359 'level' => 3, 360 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, 361 'node' => $this, 362 ]; 363 } 364 365 return $messages; 366 } 367} 368