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