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 public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) 74 { 75 // There's two types of multi-valued text properties: 76 // 1. multivalue properties. 77 // 2. structured value properties 78 // 79 // The former is always separated by a comma, the latter by semi-colon. 80 if (in_array($name, $this->structuredValues)) { 81 $this->delimiter = ';'; 82 } 83 84 parent::__construct($root, $name, $value, $parameters, $group); 85 } 86 87 /** 88 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 89 * 90 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 91 * not yet done, but parameters are not included. 92 * 93 * @param string $val 94 */ 95 public function setRawMimeDirValue($val) 96 { 97 $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); 98 } 99 100 /** 101 * Sets the value as a quoted-printable encoded string. 102 * 103 * @param string $val 104 */ 105 public function setQuotedPrintableValue($val) 106 { 107 $val = quoted_printable_decode($val); 108 109 // Quoted printable only appears in vCard 2.1, and the only character 110 // that may be escaped there is ;. So we are simply splitting on just 111 // that. 112 // 113 // We also don't have to unescape \\, so all we need to look for is a ; 114 // that's not preceded with a \. 115 $regex = '# (?<!\\\\) ; #x'; 116 $matches = preg_split($regex, $val); 117 $this->setValue($matches); 118 } 119 120 /** 121 * Returns a raw mime-dir representation of the value. 122 * 123 * @return string 124 */ 125 public function getRawMimeDirValue() 126 { 127 $val = $this->getParts(); 128 129 if (isset($this->minimumPropertyValues[$this->name])) { 130 $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); 131 } 132 133 foreach ($val as &$item) { 134 if (!is_array($item)) { 135 $item = [$item]; 136 } 137 138 foreach ($item as &$subItem) { 139 $subItem = strtr( 140 $subItem, 141 [ 142 '\\' => '\\\\', 143 ';' => '\;', 144 ',' => '\,', 145 "\n" => '\n', 146 "\r" => '', 147 ] 148 ); 149 } 150 $item = implode(',', $item); 151 } 152 153 return implode($this->delimiter, $val); 154 } 155 156 /** 157 * Returns the value, in the format it should be encoded for json. 158 * 159 * This method must always return an array. 160 * 161 * @return array 162 */ 163 public function getJsonValue() 164 { 165 // Structured text values should always be returned as a single 166 // array-item. Multi-value text should be returned as multiple items in 167 // the top-array. 168 if (in_array($this->name, $this->structuredValues)) { 169 return [$this->getParts()]; 170 } 171 172 return $this->getParts(); 173 } 174 175 /** 176 * Returns the type of value. 177 * 178 * This corresponds to the VALUE= parameter. Every property also has a 179 * 'default' valueType. 180 * 181 * @return string 182 */ 183 public function getValueType() 184 { 185 return 'TEXT'; 186 } 187 188 /** 189 * Turns the object back into a serialized blob. 190 * 191 * @return string 192 */ 193 public function serialize() 194 { 195 // We need to kick in a special type of encoding, if it's a 2.1 vcard. 196 if (Document::VCARD21 !== $this->root->getDocumentType()) { 197 return parent::serialize(); 198 } 199 200 $val = $this->getParts(); 201 202 if (isset($this->minimumPropertyValues[$this->name])) { 203 $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); 204 } 205 206 // Imploding multiple parts into a single value, and splitting the 207 // values with ;. 208 if (\count($val) > 1) { 209 foreach ($val as $k => $v) { 210 $val[$k] = \str_replace(';', '\;', $v); 211 } 212 $val = \implode(';', $val); 213 } else { 214 $val = $val[0]; 215 } 216 217 $str = $this->name; 218 if ($this->group) { 219 $str = $this->group.'.'.$this->name; 220 } 221 foreach ($this->parameters as $param) { 222 if ('QUOTED-PRINTABLE' === $param->getValue()) { 223 continue; 224 } 225 $str .= ';'.$param->serialize(); 226 } 227 228 // If the resulting value contains a \n, we must encode it as 229 // quoted-printable. 230 if (false !== \strpos($val, "\n")) { 231 $str .= ';ENCODING=QUOTED-PRINTABLE:'; 232 $lastLine = $str; 233 $out = null; 234 235 // The PHP built-in quoted-printable-encode does not correctly 236 // encode newlines for us. Specifically, the \r\n sequence must in 237 // vcards be encoded as =0D=OA and we must insert soft-newlines 238 // every 75 bytes. 239 for ($ii = 0; $ii < \strlen($val); ++$ii) { 240 $ord = \ord($val[$ii]); 241 // These characters are encoded as themselves. 242 if ($ord >= 32 && $ord <= 126) { 243 $lastLine .= $val[$ii]; 244 } else { 245 $lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); 246 } 247 if (\strlen($lastLine) >= 75) { 248 // Soft line break 249 $out .= $lastLine."=\r\n "; 250 $lastLine = null; 251 } 252 } 253 if (!\is_null($lastLine)) { 254 $out .= $lastLine."\r\n"; 255 } 256 257 return $out; 258 } else { 259 $str .= ':'.$val; 260 261 $str = \preg_replace( 262 '/( 263 (?:^.)? # 1 additional byte in first line because of missing single space (see next line) 264 .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) 265 (?![\x80-\xbf]) # prevent splitting multibyte characters 266 )/x', 267 "$1\r\n ", 268 $str 269 ); 270 271 // remove single space after last CRLF 272 return \substr($str, 0, -1); 273 } 274 } 275 276 /** 277 * This method serializes only the value of a property. This is used to 278 * create xCard or xCal documents. 279 * 280 * @param Xml\Writer $writer XML writer 281 */ 282 protected function xmlSerializeValue(Xml\Writer $writer) 283 { 284 $values = $this->getParts(); 285 286 $map = function ($items) use ($values, $writer) { 287 foreach ($items as $i => $item) { 288 $writer->writeElement( 289 $item, 290 !empty($values[$i]) ? $values[$i] : null 291 ); 292 } 293 }; 294 295 switch ($this->name) { 296 // Special-casing the REQUEST-STATUS property. 297 // 298 // See: 299 // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 300 case 'REQUEST-STATUS': 301 $writer->writeElement('code', $values[0]); 302 $writer->writeElement('description', $values[1]); 303 304 if (isset($values[2])) { 305 $writer->writeElement('data', $values[2]); 306 } 307 break; 308 309 case 'N': 310 $map([ 311 'surname', 312 'given', 313 'additional', 314 'prefix', 315 'suffix', 316 ]); 317 break; 318 319 case 'GENDER': 320 $map([ 321 'sex', 322 'text', 323 ]); 324 break; 325 326 case 'ADR': 327 $map([ 328 'pobox', 329 'ext', 330 'street', 331 'locality', 332 'region', 333 'code', 334 'country', 335 ]); 336 break; 337 338 case 'CLIENTPIDMAP': 339 $map([ 340 'sourceid', 341 'uri', 342 ]); 343 break; 344 345 default: 346 parent::xmlSerializeValue($writer); 347 } 348 } 349 350 /** 351 * Validates the node for correctness. 352 * 353 * The following options are supported: 354 * - Node::REPAIR - If something is broken, and automatic repair may 355 * be attempted. 356 * 357 * An array is returned with warnings. 358 * 359 * Every item in the array has the following properties: 360 * * level - (number between 1 and 3 with severity information) 361 * * message - (human readable message) 362 * * node - (reference to the offending node) 363 * 364 * @param int $options 365 * 366 * @return array 367 */ 368 public function validate($options = 0) 369 { 370 $warnings = parent::validate($options); 371 372 if (isset($this->minimumPropertyValues[$this->name])) { 373 $minimum = $this->minimumPropertyValues[$this->name]; 374 $parts = $this->getParts(); 375 if (count($parts) < $minimum) { 376 $warnings[] = [ 377 'level' => $options & self::REPAIR ? 1 : 3, 378 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts), 379 'node' => $this, 380 ]; 381 if ($options & self::REPAIR) { 382 $parts = array_pad($parts, $minimum, ''); 383 $this->setParts($parts); 384 } 385 } 386 } 387 388 return $warnings; 389 } 390} 391