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