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