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