1<?php 2 3namespace Sabre\VObject; 4 5use Sabre\Xml; 6 7/** 8 * Property. 9 * 10 * A property is always in a KEY:VALUE structure, and may optionally contain 11 * parameters. 12 * 13 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 14 * @author Evert Pot (http://evertpot.com/) 15 * @license http://sabre.io/license/ Modified BSD License 16 */ 17abstract class Property extends Node 18{ 19 /** 20 * Property name. 21 * 22 * This will contain a string such as DTSTART, SUMMARY, FN. 23 * 24 * @var string 25 */ 26 public $name; 27 28 /** 29 * Property group. 30 * 31 * This is only used in vcards 32 * 33 * @var string 34 */ 35 public $group; 36 37 /** 38 * List of parameters. 39 * 40 * @var array 41 */ 42 public $parameters = []; 43 44 /** 45 * Current value. 46 * 47 * @var mixed 48 */ 49 protected $value; 50 51 /** 52 * In case this is a multi-value property. This string will be used as a 53 * delimiter. 54 * 55 * @var string|null 56 */ 57 public $delimiter = ';'; 58 59 /** 60 * Creates the generic property. 61 * 62 * Parameters must be specified in key=>value syntax. 63 * 64 * @param Component $root The root document 65 * @param string $name 66 * @param string|array|null $value 67 * @param array $parameters List of parameters 68 * @param string $group The vcard property group 69 */ 70 public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) 71 { 72 $this->name = $name; 73 $this->group = $group; 74 75 $this->root = $root; 76 77 foreach ($parameters as $k => $v) { 78 $this->add($k, $v); 79 } 80 81 if (!is_null($value)) { 82 $this->setValue($value); 83 } 84 } 85 86 /** 87 * Updates the current value. 88 * 89 * This may be either a single, or multiple strings in an array. 90 * 91 * @param string|array $value 92 */ 93 public function setValue($value) 94 { 95 $this->value = $value; 96 } 97 98 /** 99 * Returns the current value. 100 * 101 * This method will always return a singular value. If this was a 102 * multi-value object, some decision will be made first on how to represent 103 * it as a string. 104 * 105 * To get the correct multi-value version, use getParts. 106 * 107 * @return string 108 */ 109 public function getValue() 110 { 111 if (is_array($this->value)) { 112 if (0 == count($this->value)) { 113 return; 114 } elseif (1 === count($this->value)) { 115 return $this->value[0]; 116 } else { 117 return $this->getRawMimeDirValue(); 118 } 119 } else { 120 return $this->value; 121 } 122 } 123 124 /** 125 * Sets a multi-valued property. 126 * 127 * @param array $parts 128 */ 129 public function setParts(array $parts) 130 { 131 $this->value = $parts; 132 } 133 134 /** 135 * Returns a multi-valued property. 136 * 137 * This method always returns an array, if there was only a single value, 138 * it will still be wrapped in an array. 139 * 140 * @return array 141 */ 142 public function getParts() 143 { 144 if (is_null($this->value)) { 145 return []; 146 } elseif (is_array($this->value)) { 147 return $this->value; 148 } else { 149 return [$this->value]; 150 } 151 } 152 153 /** 154 * Adds a new parameter. 155 * 156 * If a parameter with same name already existed, the values will be 157 * combined. 158 * If nameless parameter is added, we try to guess its name. 159 * 160 * @param string $name 161 * @param string|array|null $value 162 */ 163 public function add($name, $value = null) 164 { 165 $noName = false; 166 if (null === $name) { 167 $name = Parameter::guessParameterNameByValue($value); 168 $noName = true; 169 } 170 171 if (isset($this->parameters[strtoupper($name)])) { 172 $this->parameters[strtoupper($name)]->addValue($value); 173 } else { 174 $param = new Parameter($this->root, $name, $value); 175 $param->noName = $noName; 176 $this->parameters[$param->name] = $param; 177 } 178 } 179 180 /** 181 * Returns an iterable list of children. 182 * 183 * @return array 184 */ 185 public function parameters() 186 { 187 return $this->parameters; 188 } 189 190 /** 191 * Returns the type of value. 192 * 193 * This corresponds to the VALUE= parameter. Every property also has a 194 * 'default' valueType. 195 * 196 * @return string 197 */ 198 abstract public function getValueType(); 199 200 /** 201 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 202 * 203 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 204 * not yet done, but parameters are not included. 205 * 206 * @param string $val 207 */ 208 abstract public function setRawMimeDirValue($val); 209 210 /** 211 * Returns a raw mime-dir representation of the value. 212 * 213 * @return string 214 */ 215 abstract public function getRawMimeDirValue(); 216 217 /** 218 * Turns the object back into a serialized blob. 219 * 220 * @return string 221 */ 222 public function serialize() 223 { 224 $str = $this->name; 225 if ($this->group) { 226 $str = $this->group.'.'.$this->name; 227 } 228 229 foreach ($this->parameters() as $param) { 230 $str .= ';'.$param->serialize(); 231 } 232 233 $str .= ':'.$this->getRawMimeDirValue(); 234 235 $str = \preg_replace( 236 '/( 237 (?:^.)? # 1 additional byte in first line because of missing single space (see next line) 238 .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) 239 (?![\x80-\xbf]) # prevent splitting multibyte characters 240 )/x', 241 "$1\r\n ", 242 $str 243 ); 244 245 // remove single space after last CRLF 246 return \substr($str, 0, -1); 247 } 248 249 /** 250 * Returns the value, in the format it should be encoded for JSON. 251 * 252 * This method must always return an array. 253 * 254 * @return array 255 */ 256 public function getJsonValue() 257 { 258 return $this->getParts(); 259 } 260 261 /** 262 * Sets the JSON value, as it would appear in a jCard or jCal object. 263 * 264 * The value must always be an array. 265 * 266 * @param array $value 267 */ 268 public function setJsonValue(array $value) 269 { 270 if (1 === count($value)) { 271 $this->setValue(reset($value)); 272 } else { 273 $this->setValue($value); 274 } 275 } 276 277 /** 278 * This method returns an array, with the representation as it should be 279 * encoded in JSON. This is used to create jCard or jCal documents. 280 * 281 * @return array 282 */ 283 public function jsonSerialize() 284 { 285 $parameters = []; 286 287 foreach ($this->parameters as $parameter) { 288 if ('VALUE' === $parameter->name) { 289 continue; 290 } 291 $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); 292 } 293 // In jCard, we need to encode the property-group as a separate 'group' 294 // parameter. 295 if ($this->group) { 296 $parameters['group'] = $this->group; 297 } 298 299 return array_merge( 300 [ 301 strtolower($this->name), 302 (object) $parameters, 303 strtolower($this->getValueType()), 304 ], 305 $this->getJsonValue() 306 ); 307 } 308 309 /** 310 * Hydrate data from a XML subtree, as it would appear in a xCard or xCal 311 * object. 312 * 313 * @param array $value 314 */ 315 public function setXmlValue(array $value) 316 { 317 $this->setJsonValue($value); 318 } 319 320 /** 321 * This method serializes the data into XML. This is used to create xCard or 322 * xCal documents. 323 * 324 * @param Xml\Writer $writer XML writer 325 */ 326 public function xmlSerialize(Xml\Writer $writer) 327 { 328 $parameters = []; 329 330 foreach ($this->parameters as $parameter) { 331 if ('VALUE' === $parameter->name) { 332 continue; 333 } 334 335 $parameters[] = $parameter; 336 } 337 338 $writer->startElement(strtolower($this->name)); 339 340 if (!empty($parameters)) { 341 $writer->startElement('parameters'); 342 343 foreach ($parameters as $parameter) { 344 $writer->startElement(strtolower($parameter->name)); 345 $writer->write($parameter); 346 $writer->endElement(); 347 } 348 349 $writer->endElement(); 350 } 351 352 $this->xmlSerializeValue($writer); 353 $writer->endElement(); 354 } 355 356 /** 357 * This method serializes only the value of a property. This is used to 358 * create xCard or xCal documents. 359 * 360 * @param Xml\Writer $writer XML writer 361 */ 362 protected function xmlSerializeValue(Xml\Writer $writer) 363 { 364 $valueType = strtolower($this->getValueType()); 365 366 foreach ($this->getJsonValue() as $values) { 367 foreach ((array) $values as $value) { 368 $writer->writeElement($valueType, $value); 369 } 370 } 371 } 372 373 /** 374 * Called when this object is being cast to a string. 375 * 376 * If the property only had a single value, you will get just that. In the 377 * case the property had multiple values, the contents will be escaped and 378 * combined with ,. 379 * 380 * @return string 381 */ 382 public function __toString() 383 { 384 return (string) $this->getValue(); 385 } 386 387 /* ArrayAccess interface {{{ */ 388 389 /** 390 * Checks if an array element exists. 391 * 392 * @param mixed $name 393 * 394 * @return bool 395 */ 396 public function offsetExists($name) 397 { 398 if (is_int($name)) { 399 return parent::offsetExists($name); 400 } 401 402 $name = strtoupper($name); 403 404 foreach ($this->parameters as $parameter) { 405 if ($parameter->name == $name) { 406 return true; 407 } 408 } 409 410 return false; 411 } 412 413 /** 414 * Returns a parameter. 415 * 416 * If the parameter does not exist, null is returned. 417 * 418 * @param string $name 419 * 420 * @return Node 421 */ 422 public function offsetGet($name) 423 { 424 if (is_int($name)) { 425 return parent::offsetGet($name); 426 } 427 $name = strtoupper($name); 428 429 if (!isset($this->parameters[$name])) { 430 return; 431 } 432 433 return $this->parameters[$name]; 434 } 435 436 /** 437 * Creates a new parameter. 438 * 439 * @param string $name 440 * @param mixed $value 441 */ 442 public function offsetSet($name, $value) 443 { 444 if (is_int($name)) { 445 parent::offsetSet($name, $value); 446 // @codeCoverageIgnoreStart 447 // This will never be reached, because an exception is always 448 // thrown. 449 return; 450 // @codeCoverageIgnoreEnd 451 } 452 453 $param = new Parameter($this->root, $name, $value); 454 $this->parameters[$param->name] = $param; 455 } 456 457 /** 458 * Removes one or more parameters with the specified name. 459 * 460 * @param string $name 461 */ 462 public function offsetUnset($name) 463 { 464 if (is_int($name)) { 465 parent::offsetUnset($name); 466 // @codeCoverageIgnoreStart 467 // This will never be reached, because an exception is always 468 // thrown. 469 return; 470 // @codeCoverageIgnoreEnd 471 } 472 473 unset($this->parameters[strtoupper($name)]); 474 } 475 476 /* }}} */ 477 478 /** 479 * This method is automatically called when the object is cloned. 480 * Specifically, this will ensure all child elements are also cloned. 481 */ 482 public function __clone() 483 { 484 foreach ($this->parameters as $key => $child) { 485 $this->parameters[$key] = clone $child; 486 $this->parameters[$key]->parent = $this; 487 } 488 } 489 490 /** 491 * Validates the node for correctness. 492 * 493 * The following options are supported: 494 * - Node::REPAIR - If something is broken, and automatic repair may 495 * be attempted. 496 * 497 * An array is returned with warnings. 498 * 499 * Every item in the array has the following properties: 500 * * level - (number between 1 and 3 with severity information) 501 * * message - (human readable message) 502 * * node - (reference to the offending node) 503 * 504 * @param int $options 505 * 506 * @return array 507 */ 508 public function validate($options = 0) 509 { 510 $warnings = []; 511 512 // Checking if our value is UTF-8 513 if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { 514 $oldValue = $this->getRawMimeDirValue(); 515 $level = 3; 516 if ($options & self::REPAIR) { 517 $newValue = StringUtil::convertToUTF8($oldValue); 518 if (true || StringUtil::isUTF8($newValue)) { 519 $this->setRawMimeDirValue($newValue); 520 $level = 1; 521 } 522 } 523 524 if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) { 525 $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')'; 526 } else { 527 $message = 'Property is not valid UTF-8! '.$oldValue; 528 } 529 530 $warnings[] = [ 531 'level' => $level, 532 'message' => $message, 533 'node' => $this, 534 ]; 535 } 536 537 // Checking if the propertyname does not contain any invalid bytes. 538 if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { 539 $warnings[] = [ 540 'level' => $options & self::REPAIR ? 1 : 3, 541 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', 542 'node' => $this, 543 ]; 544 if ($options & self::REPAIR) { 545 // Uppercasing and converting underscores to dashes. 546 $this->name = strtoupper( 547 str_replace('_', '-', $this->name) 548 ); 549 // Removing every other invalid character 550 $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); 551 } 552 } 553 554 if ($encoding = $this->offsetGet('ENCODING')) { 555 if (Document::VCARD40 === $this->root->getDocumentType()) { 556 $warnings[] = [ 557 'level' => 3, 558 'message' => 'ENCODING parameter is not valid in vCard 4.', 559 'node' => $this, 560 ]; 561 } else { 562 $encoding = (string) $encoding; 563 564 $allowedEncoding = []; 565 566 switch ($this->root->getDocumentType()) { 567 case Document::ICALENDAR20: 568 $allowedEncoding = ['8BIT', 'BASE64']; 569 break; 570 case Document::VCARD21: 571 $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT']; 572 break; 573 case Document::VCARD30: 574 $allowedEncoding = ['B']; 575 //Repair vCard30 that use BASE64 encoding 576 if ($options & self::REPAIR) { 577 if ('BASE64' === strtoupper($encoding)) { 578 $encoding = 'B'; 579 $this['ENCODING'] = $encoding; 580 $warnings[] = [ 581 'level' => 1, 582 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.', 583 'node' => $this, 584 ]; 585 } 586 } 587 break; 588 } 589 if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { 590 $warnings[] = [ 591 'level' => 3, 592 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', 593 'node' => $this, 594 ]; 595 } 596 } 597 } 598 599 // Validating inner parameters 600 foreach ($this->parameters as $param) { 601 $warnings = array_merge($warnings, $param->validate($options)); 602 } 603 604 return $warnings; 605 } 606 607 /** 608 * Call this method on a document if you're done using it. 609 * 610 * It's intended to remove all circular references, so PHP can easily clean 611 * it up. 612 */ 613 public function destroy() 614 { 615 parent::destroy(); 616 foreach ($this->parameters as $param) { 617 $param->destroy(); 618 } 619 $this->parameters = []; 620 } 621} 622