1<?php 2 3namespace Sabre\VObject\Property\VCard; 4 5use DateTime; 6use DateTimeImmutable; 7use DateTimeInterface; 8use Sabre\VObject\DateTimeParser; 9use Sabre\VObject\InvalidDataException; 10use Sabre\VObject\Property; 11use Sabre\Xml; 12 13/** 14 * DateAndOrTime property. 15 * 16 * This object encodes DATE-AND-OR-TIME values. 17 * 18 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 19 * @author Evert Pot (http://evertpot.com/) 20 * @license http://sabre.io/license/ Modified BSD License 21 */ 22class DateAndOrTime extends Property { 23 24 /** 25 * Field separator. 26 * 27 * @var null|string 28 */ 29 public $delimiter = null; 30 31 /** 32 * Returns the type of value. 33 * 34 * This corresponds to the VALUE= parameter. Every property also has a 35 * 'default' valueType. 36 * 37 * @return string 38 */ 39 function getValueType() { 40 41 return 'DATE-AND-OR-TIME'; 42 43 } 44 45 /** 46 * Sets a multi-valued property. 47 * 48 * You may also specify DateTimeInterface objects here. 49 * 50 * @param array $parts 51 * 52 * @return void 53 */ 54 function setParts(array $parts) { 55 56 if (count($parts) > 1) { 57 throw new \InvalidArgumentException('Only one value allowed'); 58 } 59 if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) { 60 $this->setDateTime($parts[0]); 61 } else { 62 parent::setParts($parts); 63 } 64 65 } 66 67 /** 68 * Updates the current value. 69 * 70 * This may be either a single, or multiple strings in an array. 71 * 72 * Instead of strings, you may also use DateTimeInterface here. 73 * 74 * @param string|array|DateTimeInterface $value 75 * 76 * @return void 77 */ 78 function setValue($value) { 79 80 if ($value instanceof DateTimeInterface) { 81 $this->setDateTime($value); 82 } else { 83 parent::setValue($value); 84 } 85 86 } 87 88 /** 89 * Sets the property as a DateTime object. 90 * 91 * @param DateTimeInterface $dt 92 * 93 * @return void 94 */ 95 function setDateTime(DateTimeInterface $dt) { 96 97 $tz = $dt->getTimeZone(); 98 $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); 99 100 if ($isUtc) { 101 $value = $dt->format('Ymd\\THis\\Z'); 102 } else { 103 // Calculating the offset. 104 $value = $dt->format('Ymd\\THisO'); 105 } 106 107 $this->value = $value; 108 109 } 110 111 /** 112 * Returns a date-time value. 113 * 114 * Note that if this property contained more than 1 date-time, only the 115 * first will be returned. To get an array with multiple values, call 116 * getDateTimes. 117 * 118 * If no time was specified, we will always use midnight (in the default 119 * timezone) as the time. 120 * 121 * If parts of the date were omitted, such as the year, we will grab the 122 * current values for those. So at the time of writing, if the year was 123 * omitted, we would have filled in 2014. 124 * 125 * @return DateTimeImmutable 126 */ 127 function getDateTime() { 128 129 $now = new DateTime(); 130 131 $tzFormat = $now->getTimezone()->getOffset($now) === 0 ? '\\Z' : 'O'; 132 $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This' . $tzFormat)); 133 134 $dateParts = DateTimeParser::parseVCardDateTime($this->getValue()); 135 136 // This sets all the missing parts to the current date/time. 137 // So if the year was missing for a birthday, we're making it 'this 138 // year'. 139 foreach ($dateParts as $k => $v) { 140 if (is_null($v)) { 141 $dateParts[$k] = $nowParts[$k]; 142 } 143 } 144 return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]"); 145 146 } 147 148 /** 149 * Returns the value, in the format it should be encoded for json. 150 * 151 * This method must always return an array. 152 * 153 * @return array 154 */ 155 function getJsonValue() { 156 157 $parts = DateTimeParser::parseVCardDateTime($this->getValue()); 158 159 $dateStr = ''; 160 161 // Year 162 if (!is_null($parts['year'])) { 163 164 $dateStr .= $parts['year']; 165 166 if (!is_null($parts['month'])) { 167 // If a year and a month is set, we need to insert a separator 168 // dash. 169 $dateStr .= '-'; 170 } 171 172 } else { 173 174 if (!is_null($parts['month']) || !is_null($parts['date'])) { 175 // Inserting two dashes 176 $dateStr .= '--'; 177 } 178 179 } 180 181 // Month 182 if (!is_null($parts['month'])) { 183 184 $dateStr .= $parts['month']; 185 186 if (isset($parts['date'])) { 187 // If month and date are set, we need the separator dash. 188 $dateStr .= '-'; 189 } 190 191 } elseif (isset($parts['date'])) { 192 // If the month is empty, and a date is set, we need a 'empty 193 // dash' 194 $dateStr .= '-'; 195 } 196 197 // Date 198 if (!is_null($parts['date'])) { 199 $dateStr .= $parts['date']; 200 } 201 202 203 // Early exit if we don't have a time string. 204 if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { 205 return [$dateStr]; 206 } 207 208 $dateStr .= 'T'; 209 210 // Hour 211 if (!is_null($parts['hour'])) { 212 213 $dateStr .= $parts['hour']; 214 215 if (!is_null($parts['minute'])) { 216 $dateStr .= ':'; 217 } 218 219 } else { 220 // We know either minute or second _must_ be set, so we insert a 221 // dash for an empty value. 222 $dateStr .= '-'; 223 } 224 225 // Minute 226 if (!is_null($parts['minute'])) { 227 228 $dateStr .= $parts['minute']; 229 230 if (!is_null($parts['second'])) { 231 $dateStr .= ':'; 232 } 233 234 } elseif (isset($parts['second'])) { 235 // Dash for empty minute 236 $dateStr .= '-'; 237 } 238 239 // Second 240 if (!is_null($parts['second'])) { 241 $dateStr .= $parts['second']; 242 } 243 244 // Timezone 245 if (!is_null($parts['timezone'])) { 246 $dateStr .= $parts['timezone']; 247 } 248 249 return [$dateStr]; 250 251 } 252 253 /** 254 * This method serializes only the value of a property. This is used to 255 * create xCard or xCal documents. 256 * 257 * @param Xml\Writer $writer XML writer. 258 * 259 * @return void 260 */ 261 protected function xmlSerializeValue(Xml\Writer $writer) { 262 263 $valueType = strtolower($this->getValueType()); 264 $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue()); 265 $value = ''; 266 267 // $d = defined 268 $d = function($part) use ($parts) { 269 return !is_null($parts[$part]); 270 }; 271 272 // $r = read 273 $r = function($part) use ($parts) { 274 return $parts[$part]; 275 }; 276 277 // From the Relax NG Schema. 278 // 279 // # 4.3.1 280 // value-date = element date { 281 // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } 282 // } 283 if (($d('year') || $d('month') || $d('date')) 284 && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) { 285 286 if ($d('year') && $d('month') && $d('date')) { 287 $value .= $r('year') . $r('month') . $r('date'); 288 } elseif ($d('year') && $d('month') && !$d('date')) { 289 $value .= $r('year') . '-' . $r('month'); 290 } elseif (!$d('year') && $d('month')) { 291 $value .= '--' . $r('month') . $r('date'); 292 } elseif (!$d('year') && !$d('month') && $d('date')) { 293 $value .= '---' . $r('date'); 294 } 295 296 // # 4.3.2 297 // value-time = element time { 298 // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)" 299 // ~ "(Z|[+\-]\d\d(\d\d)?)?" } 300 // } 301 } elseif ((!$d('year') && !$d('month') && !$d('date')) 302 && ($d('hour') || $d('minute') || $d('second'))) { 303 304 if ($d('hour')) { 305 $value .= $r('hour') . $r('minute') . $r('second'); 306 } elseif ($d('minute')) { 307 $value .= '-' . $r('minute') . $r('second'); 308 } elseif ($d('second')) { 309 $value .= '--' . $r('second'); 310 } 311 312 $value .= $r('timezone'); 313 314 // # 4.3.3 315 // value-date-time = element date-time { 316 // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" 317 // ~ "(Z|[+\-]\d\d(\d\d)?)?" } 318 // } 319 } elseif ($d('date') && $d('hour')) { 320 321 if ($d('year') && $d('month') && $d('date')) { 322 $value .= $r('year') . $r('month') . $r('date'); 323 } elseif (!$d('year') && $d('month') && $d('date')) { 324 $value .= '--' . $r('month') . $r('date'); 325 } elseif (!$d('year') && !$d('month') && $d('date')) { 326 $value .= '---' . $r('date'); 327 } 328 329 $value .= 'T' . $r('hour') . $r('minute') . $r('second') . 330 $r('timezone'); 331 332 } 333 334 $writer->writeElement($valueType, $value); 335 336 } 337 338 /** 339 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 340 * 341 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 342 * not yet done, but parameters are not included. 343 * 344 * @param string $val 345 * 346 * @return void 347 */ 348 function setRawMimeDirValue($val) { 349 350 $this->setValue($val); 351 352 } 353 354 /** 355 * Returns a raw mime-dir representation of the value. 356 * 357 * @return string 358 */ 359 function getRawMimeDirValue() { 360 361 return implode($this->delimiter, $this->getParts()); 362 363 } 364 365 /** 366 * Validates the node for correctness. 367 * 368 * The following options are supported: 369 * Node::REPAIR - May attempt to automatically repair the problem. 370 * 371 * This method returns an array with detected problems. 372 * Every element has the following properties: 373 * 374 * * level - problem level. 375 * * message - A human-readable string describing the issue. 376 * * node - A reference to the problematic node. 377 * 378 * The level means: 379 * 1 - The issue was repaired (only happens if REPAIR was turned on) 380 * 2 - An inconsequential issue 381 * 3 - A severe issue. 382 * 383 * @param int $options 384 * 385 * @return array 386 */ 387 function validate($options = 0) { 388 389 $messages = parent::validate($options); 390 $value = $this->getValue(); 391 392 try { 393 DateTimeParser::parseVCardDateTime($value); 394 } catch (InvalidDataException $e) { 395 $messages[] = [ 396 'level' => 3, 397 'message' => 'The supplied value (' . $value . ') is not a correct DATE-AND-OR-TIME property', 398 'node' => $this, 399 ]; 400 } 401 402 return $messages; 403 404 } 405} 406