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