1<?php 2 3namespace Sabre\VObject\Property; 4 5use Sabre\VObject\Component; 6use Sabre\VObject\Document; 7use Sabre\VObject\Parser\MimeDir; 8use Sabre\VObject\Property; 9use Sabre\Xml; 10 11/** 12 * Text property. 13 * 14 * This object represents TEXT values. 15 * 16 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 17 * @author Evert Pot (http://evertpot.com/) 18 * @license http://sabre.io/license/ Modified BSD License 19 */ 20class Text extends Property { 21 22 /** 23 * In case this is a multi-value property. This string will be used as a 24 * delimiter. 25 * 26 * @var string 27 */ 28 public $delimiter = ','; 29 30 /** 31 * List of properties that are considered 'structured'. 32 * 33 * @var array 34 */ 35 protected $structuredValues = [ 36 // vCard 37 'N', 38 'ADR', 39 'ORG', 40 'GENDER', 41 'CLIENTPIDMAP', 42 43 // iCalendar 44 'REQUEST-STATUS', 45 ]; 46 47 /** 48 * Some text components have a minimum number of components. 49 * 50 * N must for instance be represented as 5 components, separated by ;, even 51 * if the last few components are unused. 52 * 53 * @var array 54 */ 55 protected $minimumPropertyValues = [ 56 'N' => 5, 57 'ADR' => 7, 58 ]; 59 60 /** 61 * Creates the property. 62 * 63 * You can specify the parameters either in key=>value syntax, in which case 64 * parameters will automatically be created, or you can just pass a list of 65 * Parameter objects. 66 * 67 * @param Component $root The root document 68 * @param string $name 69 * @param string|array|null $value 70 * @param array $parameters List of parameters 71 * @param string $group The vcard property group 72 * 73 * @return void 74 */ 75 function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) { 76 77 // There's two types of multi-valued text properties: 78 // 1. multivalue properties. 79 // 2. structured value properties 80 // 81 // The former is always separated by a comma, the latter by semi-colon. 82 if (in_array($name, $this->structuredValues)) { 83 $this->delimiter = ';'; 84 } 85 86 parent::__construct($root, $name, $value, $parameters, $group); 87 88 } 89 90 /** 91 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 92 * 93 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 94 * not yet done, but parameters are not included. 95 * 96 * @param string $val 97 * 98 * @return void 99 */ 100 function setRawMimeDirValue($val) { 101 102 $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); 103 104 } 105 106 /** 107 * Sets the value as a quoted-printable encoded string. 108 * 109 * @param string $val 110 * 111 * @return void 112 */ 113 function setQuotedPrintableValue($val) { 114 115 $val = quoted_printable_decode($val); 116 117 // Quoted printable only appears in vCard 2.1, and the only character 118 // that may be escaped there is ;. So we are simply splitting on just 119 // that. 120 // 121 // We also don't have to unescape \\, so all we need to look for is a ; 122 // that's not preceeded with a \. 123 $regex = '# (?<!\\\\) ; #x'; 124 $matches = preg_split($regex, $val); 125 $this->setValue($matches); 126 127 } 128 129 /** 130 * Returns a raw mime-dir representation of the value. 131 * 132 * @return string 133 */ 134 function getRawMimeDirValue() { 135 136 $val = $this->getParts(); 137 138 if (isset($this->minimumPropertyValues[$this->name])) { 139 $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); 140 } 141 142 foreach ($val as &$item) { 143 144 if (!is_array($item)) { 145 $item = [$item]; 146 } 147 148 foreach ($item as &$subItem) { 149 $subItem = strtr( 150 $subItem, 151 [ 152 '\\' => '\\\\', 153 ';' => '\;', 154 ',' => '\,', 155 "\n" => '\n', 156 "\r" => "", 157 ] 158 ); 159 } 160 $item = implode(',', $item); 161 162 } 163 164 return implode($this->delimiter, $val); 165 166 } 167 168 /** 169 * Returns the value, in the format it should be encoded for json. 170 * 171 * This method must always return an array. 172 * 173 * @return array 174 */ 175 function getJsonValue() { 176 177 // Structured text values should always be returned as a single 178 // array-item. Multi-value text should be returned as multiple items in 179 // the top-array. 180 if (in_array($this->name, $this->structuredValues)) { 181 return [$this->getParts()]; 182 } 183 return $this->getParts(); 184 185 } 186 187 /** 188 * Returns the type of value. 189 * 190 * This corresponds to the VALUE= parameter. Every property also has a 191 * 'default' valueType. 192 * 193 * @return string 194 */ 195 function getValueType() { 196 197 return 'TEXT'; 198 199 } 200 201 /** 202 * Turns the object back into a serialized blob. 203 * 204 * @return string 205 */ 206 function serialize() { 207 208 // We need to kick in a special type of encoding, if it's a 2.1 vcard. 209 if ($this->root->getDocumentType() !== Document::VCARD21) { 210 return parent::serialize(); 211 } 212 213 $val = $this->getParts(); 214 215 if (isset($this->minimumPropertyValues[$this->name])) { 216 $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); 217 } 218 219 // Imploding multiple parts into a single value, and splitting the 220 // values with ;. 221 if (count($val) > 1) { 222 foreach ($val as $k => $v) { 223 $val[$k] = str_replace(';', '\;', $v); 224 } 225 $val = implode(';', $val); 226 } else { 227 $val = $val[0]; 228 } 229 230 $str = $this->name; 231 if ($this->group) $str = $this->group . '.' . $this->name; 232 foreach ($this->parameters as $param) { 233 234 if ($param->getValue() === 'QUOTED-PRINTABLE') { 235 continue; 236 } 237 $str .= ';' . $param->serialize(); 238 239 } 240 241 242 243 // If the resulting value contains a \n, we must encode it as 244 // quoted-printable. 245 if (strpos($val, "\n") !== false) { 246 247 $str .= ';ENCODING=QUOTED-PRINTABLE:'; 248 $lastLine = $str; 249 $out = null; 250 251 // The PHP built-in quoted-printable-encode does not correctly 252 // encode newlines for us. Specifically, the \r\n sequence must in 253 // vcards be encoded as =0D=OA and we must insert soft-newlines 254 // every 75 bytes. 255 for ($ii = 0;$ii < strlen($val);$ii++) { 256 $ord = ord($val[$ii]); 257 // These characters are encoded as themselves. 258 if ($ord >= 32 && $ord <= 126) { 259 $lastLine .= $val[$ii]; 260 } else { 261 $lastLine .= '=' . strtoupper(bin2hex($val[$ii])); 262 } 263 if (strlen($lastLine) >= 75) { 264 // Soft line break 265 $out .= $lastLine . "=\r\n "; 266 $lastLine = null; 267 } 268 269 } 270 if (!is_null($lastLine)) $out .= $lastLine . "\r\n"; 271 return $out; 272 273 } else { 274 $str .= ':' . $val; 275 $out = ''; 276 while (strlen($str) > 0) { 277 if (strlen($str) > 75) { 278 $out .= mb_strcut($str, 0, 75, 'utf-8') . "\r\n"; 279 $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8'); 280 } else { 281 $out .= $str . "\r\n"; 282 $str = ''; 283 break; 284 } 285 } 286 287 return $out; 288 289 } 290 291 } 292 293 /** 294 * This method serializes only the value of a property. This is used to 295 * create xCard or xCal documents. 296 * 297 * @param Xml\Writer $writer XML writer. 298 * 299 * @return void 300 */ 301 protected function xmlSerializeValue(Xml\Writer $writer) { 302 303 $values = $this->getParts(); 304 305 $map = function($items) use ($values, $writer) { 306 foreach ($items as $i => $item) { 307 $writer->writeElement( 308 $item, 309 !empty($values[$i]) ? $values[$i] : null 310 ); 311 } 312 }; 313 314 switch ($this->name) { 315 316 // Special-casing the REQUEST-STATUS property. 317 // 318 // See: 319 // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 320 case 'REQUEST-STATUS': 321 $writer->writeElement('code', $values[0]); 322 $writer->writeElement('description', $values[1]); 323 324 if (isset($values[2])) { 325 $writer->writeElement('data', $values[2]); 326 } 327 break; 328 329 case 'N': 330 $map([ 331 'surname', 332 'given', 333 'additional', 334 'prefix', 335 'suffix' 336 ]); 337 break; 338 339 case 'GENDER': 340 $map([ 341 'sex', 342 'text' 343 ]); 344 break; 345 346 case 'ADR': 347 $map([ 348 'pobox', 349 'ext', 350 'street', 351 'locality', 352 'region', 353 'code', 354 'country' 355 ]); 356 break; 357 358 case 'CLIENTPIDMAP': 359 $map([ 360 'sourceid', 361 'uri' 362 ]); 363 break; 364 365 default: 366 parent::xmlSerializeValue($writer); 367 } 368 369 } 370 371 /** 372 * Validates the node for correctness. 373 * 374 * The following options are supported: 375 * - Node::REPAIR - If something is broken, and automatic repair may 376 * be attempted. 377 * 378 * An array is returned with warnings. 379 * 380 * Every item in the array has the following properties: 381 * * level - (number between 1 and 3 with severity information) 382 * * message - (human readable message) 383 * * node - (reference to the offending node) 384 * 385 * @param int $options 386 * 387 * @return array 388 */ 389 function validate($options = 0) { 390 391 $warnings = parent::validate($options); 392 393 if (isset($this->minimumPropertyValues[$this->name])) { 394 395 $minimum = $this->minimumPropertyValues[$this->name]; 396 $parts = $this->getParts(); 397 if (count($parts) < $minimum) { 398 $warnings[] = [ 399 'level' => $options & self::REPAIR ? 1 : 3, 400 'message' => 'The ' . $this->name . ' property must have at least ' . $minimum . ' values. It only has ' . count($parts), 401 'node' => $this, 402 ]; 403 if ($options & self::REPAIR) { 404 $parts = array_pad($parts, $minimum, ''); 405 $this->setParts($parts); 406 } 407 } 408 409 } 410 return $warnings; 411 412 } 413} 414