1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\VObject\Parser; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse 6*a1a3b679SAndreas Boehler Sabre\VObject\ParseException, 7*a1a3b679SAndreas Boehler Sabre\VObject\EofException, 8*a1a3b679SAndreas Boehler Sabre\VObject\Component, 9*a1a3b679SAndreas Boehler Sabre\VObject\Property, 10*a1a3b679SAndreas Boehler Sabre\VObject\Component\VCalendar, 11*a1a3b679SAndreas Boehler Sabre\VObject\Component\VCard; 12*a1a3b679SAndreas Boehler 13*a1a3b679SAndreas Boehler/** 14*a1a3b679SAndreas Boehler * MimeDir parser. 15*a1a3b679SAndreas Boehler * 16*a1a3b679SAndreas Boehler * This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This 17*a1a3b679SAndreas Boehler * parser will return one of the following two objects from the parse method: 18*a1a3b679SAndreas Boehler * 19*a1a3b679SAndreas Boehler * Sabre\VObject\Component\VCalendar 20*a1a3b679SAndreas Boehler * Sabre\VObject\Component\VCard 21*a1a3b679SAndreas Boehler * 22*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). 23*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 24*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 25*a1a3b679SAndreas Boehler */ 26*a1a3b679SAndreas Boehlerclass MimeDir extends Parser { 27*a1a3b679SAndreas Boehler 28*a1a3b679SAndreas Boehler /** 29*a1a3b679SAndreas Boehler * The input stream. 30*a1a3b679SAndreas Boehler * 31*a1a3b679SAndreas Boehler * @var resource 32*a1a3b679SAndreas Boehler */ 33*a1a3b679SAndreas Boehler protected $input; 34*a1a3b679SAndreas Boehler 35*a1a3b679SAndreas Boehler /** 36*a1a3b679SAndreas Boehler * Root component 37*a1a3b679SAndreas Boehler * 38*a1a3b679SAndreas Boehler * @var Component 39*a1a3b679SAndreas Boehler */ 40*a1a3b679SAndreas Boehler protected $root; 41*a1a3b679SAndreas Boehler 42*a1a3b679SAndreas Boehler /** 43*a1a3b679SAndreas Boehler * Parses an iCalendar or vCard file 44*a1a3b679SAndreas Boehler * 45*a1a3b679SAndreas Boehler * Pass a stream or a string. If null is parsed, the existing buffer is 46*a1a3b679SAndreas Boehler * used. 47*a1a3b679SAndreas Boehler * 48*a1a3b679SAndreas Boehler * @param string|resource|null $input 49*a1a3b679SAndreas Boehler * @param int|null $options 50*a1a3b679SAndreas Boehler * @return array 51*a1a3b679SAndreas Boehler */ 52*a1a3b679SAndreas Boehler public function parse($input = null, $options = null) { 53*a1a3b679SAndreas Boehler 54*a1a3b679SAndreas Boehler $this->root = null; 55*a1a3b679SAndreas Boehler if (!is_null($input)) { 56*a1a3b679SAndreas Boehler 57*a1a3b679SAndreas Boehler $this->setInput($input); 58*a1a3b679SAndreas Boehler 59*a1a3b679SAndreas Boehler } 60*a1a3b679SAndreas Boehler 61*a1a3b679SAndreas Boehler if (!is_null($options)) $this->options = $options; 62*a1a3b679SAndreas Boehler 63*a1a3b679SAndreas Boehler $this->parseDocument(); 64*a1a3b679SAndreas Boehler 65*a1a3b679SAndreas Boehler return $this->root; 66*a1a3b679SAndreas Boehler 67*a1a3b679SAndreas Boehler } 68*a1a3b679SAndreas Boehler 69*a1a3b679SAndreas Boehler /** 70*a1a3b679SAndreas Boehler * Sets the input buffer. Must be a string or stream. 71*a1a3b679SAndreas Boehler * 72*a1a3b679SAndreas Boehler * @param resource|string $input 73*a1a3b679SAndreas Boehler * @return void 74*a1a3b679SAndreas Boehler */ 75*a1a3b679SAndreas Boehler public function setInput($input) { 76*a1a3b679SAndreas Boehler 77*a1a3b679SAndreas Boehler // Resetting the parser 78*a1a3b679SAndreas Boehler $this->lineIndex = 0; 79*a1a3b679SAndreas Boehler $this->startLine = 0; 80*a1a3b679SAndreas Boehler 81*a1a3b679SAndreas Boehler if (is_string($input)) { 82*a1a3b679SAndreas Boehler // Convering to a stream. 83*a1a3b679SAndreas Boehler $stream = fopen('php://temp', 'r+'); 84*a1a3b679SAndreas Boehler fwrite($stream, $input); 85*a1a3b679SAndreas Boehler rewind($stream); 86*a1a3b679SAndreas Boehler $this->input = $stream; 87*a1a3b679SAndreas Boehler } elseif (is_resource($input)) { 88*a1a3b679SAndreas Boehler $this->input = $input; 89*a1a3b679SAndreas Boehler } else { 90*a1a3b679SAndreas Boehler throw new \InvalidArgumentException('This parser can only read from strings or streams.'); 91*a1a3b679SAndreas Boehler } 92*a1a3b679SAndreas Boehler 93*a1a3b679SAndreas Boehler } 94*a1a3b679SAndreas Boehler 95*a1a3b679SAndreas Boehler /** 96*a1a3b679SAndreas Boehler * Parses an entire document. 97*a1a3b679SAndreas Boehler * 98*a1a3b679SAndreas Boehler * @return void 99*a1a3b679SAndreas Boehler */ 100*a1a3b679SAndreas Boehler protected function parseDocument() { 101*a1a3b679SAndreas Boehler 102*a1a3b679SAndreas Boehler $line = $this->readLine(); 103*a1a3b679SAndreas Boehler 104*a1a3b679SAndreas Boehler // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). 105*a1a3b679SAndreas Boehler // It's 0xEF 0xBB 0xBF in UTF-8 hex. 106*a1a3b679SAndreas Boehler if ( 3 <= strlen($line) 107*a1a3b679SAndreas Boehler && ord($line[0]) === 0xef 108*a1a3b679SAndreas Boehler && ord($line[1]) === 0xbb 109*a1a3b679SAndreas Boehler && ord($line[2]) === 0xbf) { 110*a1a3b679SAndreas Boehler $line = substr($line, 3); 111*a1a3b679SAndreas Boehler } 112*a1a3b679SAndreas Boehler 113*a1a3b679SAndreas Boehler switch(strtoupper($line)) { 114*a1a3b679SAndreas Boehler case 'BEGIN:VCALENDAR' : 115*a1a3b679SAndreas Boehler $class = isset(VCalendar::$componentMap['VCALENDAR']) 116*a1a3b679SAndreas Boehler ? VCalendar::$componentMap[$name] 117*a1a3b679SAndreas Boehler : 'Sabre\\VObject\\Component\\VCalendar'; 118*a1a3b679SAndreas Boehler break; 119*a1a3b679SAndreas Boehler case 'BEGIN:VCARD' : 120*a1a3b679SAndreas Boehler $class = isset(VCard::$componentMap['VCARD']) 121*a1a3b679SAndreas Boehler ? VCard::$componentMap['VCARD'] 122*a1a3b679SAndreas Boehler : 'Sabre\\VObject\\Component\\VCard'; 123*a1a3b679SAndreas Boehler break; 124*a1a3b679SAndreas Boehler default : 125*a1a3b679SAndreas Boehler throw new ParseException('This parser only supports VCARD and VCALENDAR files'); 126*a1a3b679SAndreas Boehler } 127*a1a3b679SAndreas Boehler 128*a1a3b679SAndreas Boehler $this->root = new $class(array(), false); 129*a1a3b679SAndreas Boehler 130*a1a3b679SAndreas Boehler while(true) { 131*a1a3b679SAndreas Boehler 132*a1a3b679SAndreas Boehler // Reading until we hit END: 133*a1a3b679SAndreas Boehler $line = $this->readLine(); 134*a1a3b679SAndreas Boehler if (strtoupper(substr($line,0,4)) === 'END:') { 135*a1a3b679SAndreas Boehler break; 136*a1a3b679SAndreas Boehler } 137*a1a3b679SAndreas Boehler $result = $this->parseLine($line); 138*a1a3b679SAndreas Boehler if ($result) { 139*a1a3b679SAndreas Boehler $this->root->add($result); 140*a1a3b679SAndreas Boehler } 141*a1a3b679SAndreas Boehler 142*a1a3b679SAndreas Boehler } 143*a1a3b679SAndreas Boehler 144*a1a3b679SAndreas Boehler $name = strtoupper(substr($line, 4)); 145*a1a3b679SAndreas Boehler if ($name!==$this->root->name) { 146*a1a3b679SAndreas Boehler throw new ParseException('Invalid MimeDir file. expected: "END:' . $this->root->name . '" got: "END:' . $name . '"'); 147*a1a3b679SAndreas Boehler } 148*a1a3b679SAndreas Boehler 149*a1a3b679SAndreas Boehler } 150*a1a3b679SAndreas Boehler 151*a1a3b679SAndreas Boehler /** 152*a1a3b679SAndreas Boehler * Parses a line, and if it hits a component, it will also attempt to parse 153*a1a3b679SAndreas Boehler * the entire component 154*a1a3b679SAndreas Boehler * 155*a1a3b679SAndreas Boehler * @param string $line Unfolded line 156*a1a3b679SAndreas Boehler * @return Node 157*a1a3b679SAndreas Boehler */ 158*a1a3b679SAndreas Boehler protected function parseLine($line) { 159*a1a3b679SAndreas Boehler 160*a1a3b679SAndreas Boehler // Start of a new component 161*a1a3b679SAndreas Boehler if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') { 162*a1a3b679SAndreas Boehler 163*a1a3b679SAndreas Boehler $component = $this->root->createComponent(substr($line,6), array(), false); 164*a1a3b679SAndreas Boehler 165*a1a3b679SAndreas Boehler while(true) { 166*a1a3b679SAndreas Boehler 167*a1a3b679SAndreas Boehler // Reading until we hit END: 168*a1a3b679SAndreas Boehler $line = $this->readLine(); 169*a1a3b679SAndreas Boehler if (strtoupper(substr($line,0,4)) === 'END:') { 170*a1a3b679SAndreas Boehler break; 171*a1a3b679SAndreas Boehler } 172*a1a3b679SAndreas Boehler $result = $this->parseLine($line); 173*a1a3b679SAndreas Boehler if ($result) { 174*a1a3b679SAndreas Boehler $component->add($result); 175*a1a3b679SAndreas Boehler } 176*a1a3b679SAndreas Boehler 177*a1a3b679SAndreas Boehler } 178*a1a3b679SAndreas Boehler 179*a1a3b679SAndreas Boehler $name = strtoupper(substr($line, 4)); 180*a1a3b679SAndreas Boehler if ($name!==$component->name) { 181*a1a3b679SAndreas Boehler throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"'); 182*a1a3b679SAndreas Boehler } 183*a1a3b679SAndreas Boehler 184*a1a3b679SAndreas Boehler return $component; 185*a1a3b679SAndreas Boehler 186*a1a3b679SAndreas Boehler } else { 187*a1a3b679SAndreas Boehler 188*a1a3b679SAndreas Boehler // Property reader 189*a1a3b679SAndreas Boehler $property = $this->readProperty($line); 190*a1a3b679SAndreas Boehler if (!$property) { 191*a1a3b679SAndreas Boehler // Ignored line 192*a1a3b679SAndreas Boehler return false; 193*a1a3b679SAndreas Boehler } 194*a1a3b679SAndreas Boehler return $property; 195*a1a3b679SAndreas Boehler 196*a1a3b679SAndreas Boehler } 197*a1a3b679SAndreas Boehler 198*a1a3b679SAndreas Boehler } 199*a1a3b679SAndreas Boehler 200*a1a3b679SAndreas Boehler /** 201*a1a3b679SAndreas Boehler * We need to look ahead 1 line every time to see if we need to 'unfold' 202*a1a3b679SAndreas Boehler * the next line. 203*a1a3b679SAndreas Boehler * 204*a1a3b679SAndreas Boehler * If that was not the case, we store it here. 205*a1a3b679SAndreas Boehler * 206*a1a3b679SAndreas Boehler * @var null|string 207*a1a3b679SAndreas Boehler */ 208*a1a3b679SAndreas Boehler protected $lineBuffer; 209*a1a3b679SAndreas Boehler 210*a1a3b679SAndreas Boehler /** 211*a1a3b679SAndreas Boehler * The real current line number. 212*a1a3b679SAndreas Boehler */ 213*a1a3b679SAndreas Boehler protected $lineIndex = 0; 214*a1a3b679SAndreas Boehler 215*a1a3b679SAndreas Boehler /** 216*a1a3b679SAndreas Boehler * In the case of unfolded lines, this property holds the line number for 217*a1a3b679SAndreas Boehler * the start of the line. 218*a1a3b679SAndreas Boehler * 219*a1a3b679SAndreas Boehler * @var int 220*a1a3b679SAndreas Boehler */ 221*a1a3b679SAndreas Boehler protected $startLine = 0; 222*a1a3b679SAndreas Boehler 223*a1a3b679SAndreas Boehler /** 224*a1a3b679SAndreas Boehler * Contains a 'raw' representation of the current line. 225*a1a3b679SAndreas Boehler * 226*a1a3b679SAndreas Boehler * @var string 227*a1a3b679SAndreas Boehler */ 228*a1a3b679SAndreas Boehler protected $rawLine; 229*a1a3b679SAndreas Boehler 230*a1a3b679SAndreas Boehler /** 231*a1a3b679SAndreas Boehler * Reads a single line from the buffer. 232*a1a3b679SAndreas Boehler * 233*a1a3b679SAndreas Boehler * This method strips any newlines and also takes care of unfolding. 234*a1a3b679SAndreas Boehler * 235*a1a3b679SAndreas Boehler * @throws \Sabre\VObject\EofException 236*a1a3b679SAndreas Boehler * @return string 237*a1a3b679SAndreas Boehler */ 238*a1a3b679SAndreas Boehler protected function readLine() { 239*a1a3b679SAndreas Boehler 240*a1a3b679SAndreas Boehler if (!is_null($this->lineBuffer)) { 241*a1a3b679SAndreas Boehler $rawLine = $this->lineBuffer; 242*a1a3b679SAndreas Boehler $this->lineBuffer = null; 243*a1a3b679SAndreas Boehler } else { 244*a1a3b679SAndreas Boehler do { 245*a1a3b679SAndreas Boehler $eof = feof($this->input); 246*a1a3b679SAndreas Boehler 247*a1a3b679SAndreas Boehler $rawLine = fgets($this->input); 248*a1a3b679SAndreas Boehler 249*a1a3b679SAndreas Boehler if ($eof || (feof($this->input) && $rawLine===false)) { 250*a1a3b679SAndreas Boehler throw new EofException('End of document reached prematurely'); 251*a1a3b679SAndreas Boehler } 252*a1a3b679SAndreas Boehler if ($rawLine === false) { 253*a1a3b679SAndreas Boehler throw new ParseException('Error reading from input stream'); 254*a1a3b679SAndreas Boehler } 255*a1a3b679SAndreas Boehler $rawLine = rtrim($rawLine, "\r\n"); 256*a1a3b679SAndreas Boehler } while ($rawLine === ''); // Skipping empty lines 257*a1a3b679SAndreas Boehler $this->lineIndex++; 258*a1a3b679SAndreas Boehler } 259*a1a3b679SAndreas Boehler $line = $rawLine; 260*a1a3b679SAndreas Boehler 261*a1a3b679SAndreas Boehler $this->startLine = $this->lineIndex; 262*a1a3b679SAndreas Boehler 263*a1a3b679SAndreas Boehler // Looking ahead for folded lines. 264*a1a3b679SAndreas Boehler while (true) { 265*a1a3b679SAndreas Boehler 266*a1a3b679SAndreas Boehler $nextLine = rtrim(fgets($this->input), "\r\n"); 267*a1a3b679SAndreas Boehler $this->lineIndex++; 268*a1a3b679SAndreas Boehler if (!$nextLine) { 269*a1a3b679SAndreas Boehler break; 270*a1a3b679SAndreas Boehler } 271*a1a3b679SAndreas Boehler if ($nextLine[0] === "\t" || $nextLine[0] === " ") { 272*a1a3b679SAndreas Boehler $line .= substr($nextLine, 1); 273*a1a3b679SAndreas Boehler $rawLine .= "\n " . substr($nextLine, 1); 274*a1a3b679SAndreas Boehler } else { 275*a1a3b679SAndreas Boehler $this->lineBuffer = $nextLine; 276*a1a3b679SAndreas Boehler break; 277*a1a3b679SAndreas Boehler } 278*a1a3b679SAndreas Boehler 279*a1a3b679SAndreas Boehler } 280*a1a3b679SAndreas Boehler $this->rawLine = $rawLine; 281*a1a3b679SAndreas Boehler return $line; 282*a1a3b679SAndreas Boehler 283*a1a3b679SAndreas Boehler } 284*a1a3b679SAndreas Boehler 285*a1a3b679SAndreas Boehler /** 286*a1a3b679SAndreas Boehler * Reads a property or component from a line. 287*a1a3b679SAndreas Boehler * 288*a1a3b679SAndreas Boehler * @return void 289*a1a3b679SAndreas Boehler */ 290*a1a3b679SAndreas Boehler protected function readProperty($line) { 291*a1a3b679SAndreas Boehler 292*a1a3b679SAndreas Boehler if ($this->options & self::OPTION_FORGIVING) { 293*a1a3b679SAndreas Boehler $propNameToken = 'A-Z0-9\-\._\\/'; 294*a1a3b679SAndreas Boehler } else { 295*a1a3b679SAndreas Boehler $propNameToken = 'A-Z0-9\-\.'; 296*a1a3b679SAndreas Boehler } 297*a1a3b679SAndreas Boehler 298*a1a3b679SAndreas Boehler $paramNameToken = 'A-Z0-9\-'; 299*a1a3b679SAndreas Boehler $safeChar = '^";:,'; 300*a1a3b679SAndreas Boehler $qSafeChar = '^"'; 301*a1a3b679SAndreas Boehler 302*a1a3b679SAndreas Boehler $regex = "/ 303*a1a3b679SAndreas Boehler ^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name 304*a1a3b679SAndreas Boehler | 305*a1a3b679SAndreas Boehler (?<=:)(?P<propValue> .+)$ # property value 306*a1a3b679SAndreas Boehler | 307*a1a3b679SAndreas Boehler ;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name 308*a1a3b679SAndreas Boehler | 309*a1a3b679SAndreas Boehler (=|,)(?P<paramValue> # parameter value 310*a1a3b679SAndreas Boehler (?: [$safeChar]*) | 311*a1a3b679SAndreas Boehler \"(?: [$qSafeChar]+)\" 312*a1a3b679SAndreas Boehler ) (?=[;:,]) 313*a1a3b679SAndreas Boehler /xi"; 314*a1a3b679SAndreas Boehler 315*a1a3b679SAndreas Boehler //echo $regex, "\n"; die(); 316*a1a3b679SAndreas Boehler preg_match_all($regex, $line, $matches, PREG_SET_ORDER); 317*a1a3b679SAndreas Boehler 318*a1a3b679SAndreas Boehler $property = array( 319*a1a3b679SAndreas Boehler 'name' => null, 320*a1a3b679SAndreas Boehler 'parameters' => array(), 321*a1a3b679SAndreas Boehler 'value' => null 322*a1a3b679SAndreas Boehler ); 323*a1a3b679SAndreas Boehler 324*a1a3b679SAndreas Boehler $lastParam = null; 325*a1a3b679SAndreas Boehler 326*a1a3b679SAndreas Boehler /** 327*a1a3b679SAndreas Boehler * Looping through all the tokens. 328*a1a3b679SAndreas Boehler * 329*a1a3b679SAndreas Boehler * Note that we are looping through them in reverse order, because if a 330*a1a3b679SAndreas Boehler * sub-pattern matched, the subsequent named patterns will not show up 331*a1a3b679SAndreas Boehler * in the result. 332*a1a3b679SAndreas Boehler */ 333*a1a3b679SAndreas Boehler foreach($matches as $match) { 334*a1a3b679SAndreas Boehler 335*a1a3b679SAndreas Boehler if (isset($match['paramValue'])) { 336*a1a3b679SAndreas Boehler if ($match['paramValue'] && $match['paramValue'][0] === '"') { 337*a1a3b679SAndreas Boehler $value = substr($match['paramValue'], 1, -1); 338*a1a3b679SAndreas Boehler } else { 339*a1a3b679SAndreas Boehler $value = $match['paramValue']; 340*a1a3b679SAndreas Boehler } 341*a1a3b679SAndreas Boehler 342*a1a3b679SAndreas Boehler $value = $this->unescapeParam($value); 343*a1a3b679SAndreas Boehler 344*a1a3b679SAndreas Boehler if (is_null($property['parameters'][$lastParam])) { 345*a1a3b679SAndreas Boehler $property['parameters'][$lastParam] = $value; 346*a1a3b679SAndreas Boehler } elseif (is_array($property['parameters'][$lastParam])) { 347*a1a3b679SAndreas Boehler $property['parameters'][$lastParam][] = $value; 348*a1a3b679SAndreas Boehler } else { 349*a1a3b679SAndreas Boehler $property['parameters'][$lastParam] = array( 350*a1a3b679SAndreas Boehler $property['parameters'][$lastParam], 351*a1a3b679SAndreas Boehler $value 352*a1a3b679SAndreas Boehler ); 353*a1a3b679SAndreas Boehler } 354*a1a3b679SAndreas Boehler continue; 355*a1a3b679SAndreas Boehler } 356*a1a3b679SAndreas Boehler if (isset($match['paramName'])) { 357*a1a3b679SAndreas Boehler $lastParam = strtoupper($match['paramName']); 358*a1a3b679SAndreas Boehler if (!isset($property['parameters'][$lastParam])) { 359*a1a3b679SAndreas Boehler $property['parameters'][$lastParam] = null; 360*a1a3b679SAndreas Boehler } 361*a1a3b679SAndreas Boehler continue; 362*a1a3b679SAndreas Boehler } 363*a1a3b679SAndreas Boehler if (isset($match['propValue'])) { 364*a1a3b679SAndreas Boehler $property['value'] = $match['propValue']; 365*a1a3b679SAndreas Boehler continue; 366*a1a3b679SAndreas Boehler } 367*a1a3b679SAndreas Boehler if (isset($match['name']) && $match['name']) { 368*a1a3b679SAndreas Boehler $property['name'] = strtoupper($match['name']); 369*a1a3b679SAndreas Boehler continue; 370*a1a3b679SAndreas Boehler } 371*a1a3b679SAndreas Boehler 372*a1a3b679SAndreas Boehler // @codeCoverageIgnoreStart 373*a1a3b679SAndreas Boehler throw new \LogicException('This code should not be reachable'); 374*a1a3b679SAndreas Boehler // @codeCoverageIgnoreEnd 375*a1a3b679SAndreas Boehler 376*a1a3b679SAndreas Boehler } 377*a1a3b679SAndreas Boehler 378*a1a3b679SAndreas Boehler if (is_null($property['value'])) { 379*a1a3b679SAndreas Boehler $property['value'] = ''; 380*a1a3b679SAndreas Boehler } 381*a1a3b679SAndreas Boehler if (!$property['name']) { 382*a1a3b679SAndreas Boehler if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { 383*a1a3b679SAndreas Boehler return false; 384*a1a3b679SAndreas Boehler } 385*a1a3b679SAndreas Boehler throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions'); 386*a1a3b679SAndreas Boehler } 387*a1a3b679SAndreas Boehler 388*a1a3b679SAndreas Boehler // vCard 2.1 states that parameters may appear without a name, and only 389*a1a3b679SAndreas Boehler // a value. We can deduce the value based on it's name. 390*a1a3b679SAndreas Boehler // 391*a1a3b679SAndreas Boehler // Our parser will get those as parameters without a value instead, so 392*a1a3b679SAndreas Boehler // we're filtering these parameters out first. 393*a1a3b679SAndreas Boehler $namedParameters = array(); 394*a1a3b679SAndreas Boehler $namelessParameters = array(); 395*a1a3b679SAndreas Boehler 396*a1a3b679SAndreas Boehler foreach($property['parameters'] as $name=>$value) { 397*a1a3b679SAndreas Boehler if (!is_null($value)) { 398*a1a3b679SAndreas Boehler $namedParameters[$name] = $value; 399*a1a3b679SAndreas Boehler } else { 400*a1a3b679SAndreas Boehler $namelessParameters[] = $name; 401*a1a3b679SAndreas Boehler } 402*a1a3b679SAndreas Boehler } 403*a1a3b679SAndreas Boehler 404*a1a3b679SAndreas Boehler $propObj = $this->root->createProperty($property['name'], null, $namedParameters); 405*a1a3b679SAndreas Boehler 406*a1a3b679SAndreas Boehler foreach($namelessParameters as $namelessParameter) { 407*a1a3b679SAndreas Boehler $propObj->add(null, $namelessParameter); 408*a1a3b679SAndreas Boehler } 409*a1a3b679SAndreas Boehler 410*a1a3b679SAndreas Boehler if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') { 411*a1a3b679SAndreas Boehler $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); 412*a1a3b679SAndreas Boehler } else { 413*a1a3b679SAndreas Boehler $propObj->setRawMimeDirValue($property['value']); 414*a1a3b679SAndreas Boehler } 415*a1a3b679SAndreas Boehler 416*a1a3b679SAndreas Boehler return $propObj; 417*a1a3b679SAndreas Boehler 418*a1a3b679SAndreas Boehler } 419*a1a3b679SAndreas Boehler 420*a1a3b679SAndreas Boehler /** 421*a1a3b679SAndreas Boehler * Unescapes a property value. 422*a1a3b679SAndreas Boehler * 423*a1a3b679SAndreas Boehler * vCard 2.1 says: 424*a1a3b679SAndreas Boehler * * Semi-colons must be escaped in some property values, specifically 425*a1a3b679SAndreas Boehler * ADR, ORG and N. 426*a1a3b679SAndreas Boehler * * Semi-colons must be escaped in parameter values, because semi-colons 427*a1a3b679SAndreas Boehler * are also use to separate values. 428*a1a3b679SAndreas Boehler * * No mention of escaping backslashes with another backslash. 429*a1a3b679SAndreas Boehler * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to 430*a1a3b679SAndreas Boehler * span values over more than 1 line. 431*a1a3b679SAndreas Boehler * 432*a1a3b679SAndreas Boehler * vCard 3.0 says: 433*a1a3b679SAndreas Boehler * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be 434*a1a3b679SAndreas Boehler * escaped, all time time. 435*a1a3b679SAndreas Boehler * * Comma's are used for delimeters in multiple values 436*a1a3b679SAndreas Boehler * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, 437*a1a3b679SAndreas Boehler * as in some properties semi-colon is used for separators. 438*a1a3b679SAndreas Boehler * * Properties using semi-colons: N, ADR, GEO, ORG 439*a1a3b679SAndreas Boehler * * Both ADR and N's individual parts may be broken up further with a 440*a1a3b679SAndreas Boehler * comma. 441*a1a3b679SAndreas Boehler * * Properties using commas: NICKNAME, CATEGORIES 442*a1a3b679SAndreas Boehler * 443*a1a3b679SAndreas Boehler * vCard 4.0 (rfc6350) says: 444*a1a3b679SAndreas Boehler * * Commas must be escaped. 445*a1a3b679SAndreas Boehler * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a 446*a1a3b679SAndreas Boehler * delimiter, depending on the property. 447*a1a3b679SAndreas Boehler * * Backslashes must be escaped 448*a1a3b679SAndreas Boehler * * Newlines must be escaped as either \N or \n. 449*a1a3b679SAndreas Boehler * * Some compound properties may contain multiple parts themselves, so a 450*a1a3b679SAndreas Boehler * comma within a semi-colon delimited property may also be unescaped 451*a1a3b679SAndreas Boehler * to denote multiple parts _within_ the compound property. 452*a1a3b679SAndreas Boehler * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. 453*a1a3b679SAndreas Boehler * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. 454*a1a3b679SAndreas Boehler * 455*a1a3b679SAndreas Boehler * Even though the spec says that commas must always be escaped, the 456*a1a3b679SAndreas Boehler * example for GEO in Section 6.5.2 seems to violate this. 457*a1a3b679SAndreas Boehler * 458*a1a3b679SAndreas Boehler * iCalendar 2.0 (rfc5545) says: 459*a1a3b679SAndreas Boehler * * Commas or semi-colons may be used as delimiters, depending on the 460*a1a3b679SAndreas Boehler * property. 461*a1a3b679SAndreas Boehler * * Commas, semi-colons, backslashes, newline (\N or \n) are always 462*a1a3b679SAndreas Boehler * escaped, unless they are delimiters. 463*a1a3b679SAndreas Boehler * * Colons shall not be escaped. 464*a1a3b679SAndreas Boehler * * Commas can be considered the 'default delimiter' and is described as 465*a1a3b679SAndreas Boehler * the delimiter in cases where the order of the multiple values is 466*a1a3b679SAndreas Boehler * insignificant. 467*a1a3b679SAndreas Boehler * * Semi-colons are described as the delimiter for 'structured values'. 468*a1a3b679SAndreas Boehler * They are specifically used in Semi-colons are used as a delimiter in 469*a1a3b679SAndreas Boehler * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however. 470*a1a3b679SAndreas Boehler * 471*a1a3b679SAndreas Boehler * Now for the parameters 472*a1a3b679SAndreas Boehler * 473*a1a3b679SAndreas Boehler * If delimiter is not set (null) this method will just return a string. 474*a1a3b679SAndreas Boehler * If it's a comma or a semi-colon the string will be split on those 475*a1a3b679SAndreas Boehler * characters, and always return an array. 476*a1a3b679SAndreas Boehler * 477*a1a3b679SAndreas Boehler * @param string $input 478*a1a3b679SAndreas Boehler * @param string $delimiter 479*a1a3b679SAndreas Boehler * @return string|string[] 480*a1a3b679SAndreas Boehler */ 481*a1a3b679SAndreas Boehler static public function unescapeValue($input, $delimiter = ';') { 482*a1a3b679SAndreas Boehler 483*a1a3b679SAndreas Boehler $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; 484*a1a3b679SAndreas Boehler if ($delimiter) { 485*a1a3b679SAndreas Boehler $regex .= ' | (' . $delimiter . ')'; 486*a1a3b679SAndreas Boehler } 487*a1a3b679SAndreas Boehler $regex .= ') #x'; 488*a1a3b679SAndreas Boehler 489*a1a3b679SAndreas Boehler $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 490*a1a3b679SAndreas Boehler 491*a1a3b679SAndreas Boehler $resultArray = array(); 492*a1a3b679SAndreas Boehler $result = ''; 493*a1a3b679SAndreas Boehler 494*a1a3b679SAndreas Boehler foreach($matches as $match) { 495*a1a3b679SAndreas Boehler 496*a1a3b679SAndreas Boehler switch ($match) { 497*a1a3b679SAndreas Boehler case '\\\\' : 498*a1a3b679SAndreas Boehler $result .='\\'; 499*a1a3b679SAndreas Boehler break; 500*a1a3b679SAndreas Boehler case '\N' : 501*a1a3b679SAndreas Boehler case '\n' : 502*a1a3b679SAndreas Boehler $result .="\n"; 503*a1a3b679SAndreas Boehler break; 504*a1a3b679SAndreas Boehler case '\;' : 505*a1a3b679SAndreas Boehler $result .=';'; 506*a1a3b679SAndreas Boehler break; 507*a1a3b679SAndreas Boehler case '\,' : 508*a1a3b679SAndreas Boehler $result .=','; 509*a1a3b679SAndreas Boehler break; 510*a1a3b679SAndreas Boehler case $delimiter : 511*a1a3b679SAndreas Boehler $resultArray[] = $result; 512*a1a3b679SAndreas Boehler $result = ''; 513*a1a3b679SAndreas Boehler break; 514*a1a3b679SAndreas Boehler default : 515*a1a3b679SAndreas Boehler $result .= $match; 516*a1a3b679SAndreas Boehler break; 517*a1a3b679SAndreas Boehler 518*a1a3b679SAndreas Boehler } 519*a1a3b679SAndreas Boehler 520*a1a3b679SAndreas Boehler } 521*a1a3b679SAndreas Boehler 522*a1a3b679SAndreas Boehler $resultArray[] = $result; 523*a1a3b679SAndreas Boehler return $delimiter ? $resultArray : $result; 524*a1a3b679SAndreas Boehler 525*a1a3b679SAndreas Boehler } 526*a1a3b679SAndreas Boehler 527*a1a3b679SAndreas Boehler /** 528*a1a3b679SAndreas Boehler * Unescapes a parameter value. 529*a1a3b679SAndreas Boehler * 530*a1a3b679SAndreas Boehler * vCard 2.1: 531*a1a3b679SAndreas Boehler * * Does not mention a mechanism for this. In addition, double quotes 532*a1a3b679SAndreas Boehler * are never used to wrap values. 533*a1a3b679SAndreas Boehler * * This means that parameters can simply not contain colons or 534*a1a3b679SAndreas Boehler * semi-colons. 535*a1a3b679SAndreas Boehler * 536*a1a3b679SAndreas Boehler * vCard 3.0 (rfc2425, rfc2426): 537*a1a3b679SAndreas Boehler * * Parameters _may_ be surrounded by double quotes. 538*a1a3b679SAndreas Boehler * * If this is not the case, semi-colon, colon and comma may simply not 539*a1a3b679SAndreas Boehler * occur (the comma used for multiple parameter values though). 540*a1a3b679SAndreas Boehler * * If it is surrounded by double-quotes, it may simply not contain 541*a1a3b679SAndreas Boehler * double-quotes. 542*a1a3b679SAndreas Boehler * * This means that a parameter can in no case encode double-quotes, or 543*a1a3b679SAndreas Boehler * newlines. 544*a1a3b679SAndreas Boehler * 545*a1a3b679SAndreas Boehler * vCard 4.0 (rfc6350) 546*a1a3b679SAndreas Boehler * * Behavior seems to be identical to vCard 3.0 547*a1a3b679SAndreas Boehler * 548*a1a3b679SAndreas Boehler * iCalendar 2.0 (rfc5545) 549*a1a3b679SAndreas Boehler * * Behavior seems to be identical to vCard 3.0 550*a1a3b679SAndreas Boehler * 551*a1a3b679SAndreas Boehler * Parameter escaping mechanism (rfc6868) : 552*a1a3b679SAndreas Boehler * * This rfc describes a new way to escape parameter values. 553*a1a3b679SAndreas Boehler * * New-line is encoded as ^n 554*a1a3b679SAndreas Boehler * * ^ is encoded as ^^. 555*a1a3b679SAndreas Boehler * * " is encoded as ^' 556*a1a3b679SAndreas Boehler * 557*a1a3b679SAndreas Boehler * @param string $input 558*a1a3b679SAndreas Boehler * @return void 559*a1a3b679SAndreas Boehler */ 560*a1a3b679SAndreas Boehler private function unescapeParam($input) { 561*a1a3b679SAndreas Boehler 562*a1a3b679SAndreas Boehler return 563*a1a3b679SAndreas Boehler preg_replace_callback( 564*a1a3b679SAndreas Boehler '#(\^(\^|n|\'))#', 565*a1a3b679SAndreas Boehler function($matches) { 566*a1a3b679SAndreas Boehler switch($matches[2]) { 567*a1a3b679SAndreas Boehler case 'n' : 568*a1a3b679SAndreas Boehler return "\n"; 569*a1a3b679SAndreas Boehler case '^' : 570*a1a3b679SAndreas Boehler return '^'; 571*a1a3b679SAndreas Boehler case '\'' : 572*a1a3b679SAndreas Boehler return '"'; 573*a1a3b679SAndreas Boehler 574*a1a3b679SAndreas Boehler // @codeCoverageIgnoreStart 575*a1a3b679SAndreas Boehler } 576*a1a3b679SAndreas Boehler // @codeCoverageIgnoreEnd 577*a1a3b679SAndreas Boehler }, 578*a1a3b679SAndreas Boehler $input 579*a1a3b679SAndreas Boehler ); 580*a1a3b679SAndreas Boehler } 581*a1a3b679SAndreas Boehler 582*a1a3b679SAndreas Boehler /** 583*a1a3b679SAndreas Boehler * Gets the full quoted printable value. 584*a1a3b679SAndreas Boehler * 585*a1a3b679SAndreas Boehler * We need a special method for this, because newlines have both a meaning 586*a1a3b679SAndreas Boehler * in vCards, and in QuotedPrintable. 587*a1a3b679SAndreas Boehler * 588*a1a3b679SAndreas Boehler * This method does not do any decoding. 589*a1a3b679SAndreas Boehler * 590*a1a3b679SAndreas Boehler * @return string 591*a1a3b679SAndreas Boehler */ 592*a1a3b679SAndreas Boehler private function extractQuotedPrintableValue() { 593*a1a3b679SAndreas Boehler 594*a1a3b679SAndreas Boehler // We need to parse the raw line again to get the start of the value. 595*a1a3b679SAndreas Boehler // 596*a1a3b679SAndreas Boehler // We are basically looking for the first colon (:), but we need to 597*a1a3b679SAndreas Boehler // skip over the parameters first, as they may contain one. 598*a1a3b679SAndreas Boehler $regex = '/^ 599*a1a3b679SAndreas Boehler (?: [^:])+ # Anything but a colon 600*a1a3b679SAndreas Boehler (?: "[^"]")* # A parameter in double quotes 601*a1a3b679SAndreas Boehler : # start of the value we really care about 602*a1a3b679SAndreas Boehler (.*)$ 603*a1a3b679SAndreas Boehler /xs'; 604*a1a3b679SAndreas Boehler 605*a1a3b679SAndreas Boehler preg_match($regex, $this->rawLine, $matches); 606*a1a3b679SAndreas Boehler 607*a1a3b679SAndreas Boehler $value = $matches[1]; 608*a1a3b679SAndreas Boehler // Removing the first whitespace character from every line. Kind of 609*a1a3b679SAndreas Boehler // like unfolding, but we keep the newline. 610*a1a3b679SAndreas Boehler $value = str_replace("\n ", "\n", $value); 611*a1a3b679SAndreas Boehler 612*a1a3b679SAndreas Boehler // Microsoft products don't always correctly fold lines, they may be 613*a1a3b679SAndreas Boehler // missing a whitespace. So if 'forgiving' is turned on, we will take 614*a1a3b679SAndreas Boehler // those as well. 615*a1a3b679SAndreas Boehler if ($this->options & self::OPTION_FORGIVING) { 616*a1a3b679SAndreas Boehler while(substr($value,-1) === '=') { 617*a1a3b679SAndreas Boehler // Reading the line 618*a1a3b679SAndreas Boehler $this->readLine(); 619*a1a3b679SAndreas Boehler // Grabbing the raw form 620*a1a3b679SAndreas Boehler $value.="\n" . $this->rawLine; 621*a1a3b679SAndreas Boehler } 622*a1a3b679SAndreas Boehler } 623*a1a3b679SAndreas Boehler 624*a1a3b679SAndreas Boehler return $value; 625*a1a3b679SAndreas Boehler 626*a1a3b679SAndreas Boehler } 627*a1a3b679SAndreas Boehler 628*a1a3b679SAndreas Boehler} 629