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