ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
MimeDir.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\VObject\Parser;
4 
11 
25 class MimeDir extends Parser {
26 
32  protected $input;
33 
39  protected $root;
40 
53  protected $charset = 'UTF-8';
54 
60  protected static $SUPPORTED_CHARSETS = [
61  'UTF-8',
62  'ISO-8859-1',
63  'Windows-1252',
64  ];
65 
77  function parse($input = null, $options = 0) {
78 
79  $this->root = null;
80 
81  if (!is_null($input)) {
82  $this->setInput($input);
83  }
84 
85  if (0 !== $options) {
86  $this->options = $options;
87  }
88 
89  $this->parseDocument();
90 
91  return $this->root;
92 
93  }
94 
107  function setCharset($charset) {
108 
109  if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
110  throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: ' . implode(', ', self::$SUPPORTED_CHARSETS) . ')');
111  }
112  $this->charset = $charset;
113 
114  }
115 
123  function setInput($input) {
124 
125  // Resetting the parser
126  $this->lineIndex = 0;
127  $this->startLine = 0;
128 
129  if (is_string($input)) {
130  // Convering to a stream.
131  $stream = fopen('php://temp', 'r+');
132  fwrite($stream, $input);
133  rewind($stream);
134  $this->input = $stream;
135  } elseif (is_resource($input)) {
136  $this->input = $input;
137  } else {
138  throw new \InvalidArgumentException('This parser can only read from strings or streams.');
139  }
140 
141  }
142 
148  protected function parseDocument() {
149 
150  $line = $this->readLine();
151 
152  // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
153  // It's 0xEF 0xBB 0xBF in UTF-8 hex.
154  if (3 <= strlen($line)
155  && ord($line[0]) === 0xef
156  && ord($line[1]) === 0xbb
157  && ord($line[2]) === 0xbf) {
158  $line = substr($line, 3);
159  }
160 
161  switch (strtoupper($line)) {
162  case 'BEGIN:VCALENDAR' :
163  $class = VCalendar::$componentMap['VCALENDAR'];
164  break;
165  case 'BEGIN:VCARD' :
166  $class = VCard::$componentMap['VCARD'];
167  break;
168  default :
169  throw new ParseException('This parser only supports VCARD and VCALENDAR files');
170  }
171 
172  $this->root = new $class([], false);
173 
174  while (true) {
175 
176  // Reading until we hit END:
177  $line = $this->readLine();
178  if (strtoupper(substr($line, 0, 4)) === 'END:') {
179  break;
180  }
181  $result = $this->parseLine($line);
182  if ($result) {
183  $this->root->add($result);
184  }
185 
186  }
187 
188  $name = strtoupper(substr($line, 4));
189  if ($name !== $this->root->name) {
190  throw new ParseException('Invalid MimeDir file. expected: "END:' . $this->root->name . '" got: "END:' . $name . '"');
191  }
192 
193  }
194 
203  protected function parseLine($line) {
204 
205  // Start of a new component
206  if (strtoupper(substr($line, 0, 6)) === 'BEGIN:') {
207 
208  $component = $this->root->createComponent(substr($line, 6), [], false);
209 
210  while (true) {
211 
212  // Reading until we hit END:
213  $line = $this->readLine();
214  if (strtoupper(substr($line, 0, 4)) === 'END:') {
215  break;
216  }
217  $result = $this->parseLine($line);
218  if ($result) {
219  $component->add($result);
220  }
221 
222  }
223 
224  $name = strtoupper(substr($line, 4));
225  if ($name !== $component->name) {
226  throw new ParseException('Invalid MimeDir file. expected: "END:' . $component->name . '" got: "END:' . $name . '"');
227  }
228 
229  return $component;
230 
231  } else {
232 
233  // Property reader
234  $property = $this->readProperty($line);
235  if (!$property) {
236  // Ignored line
237  return false;
238  }
239  return $property;
240 
241  }
242 
243  }
244 
253  protected $lineBuffer;
254 
258  protected $lineIndex = 0;
259 
266  protected $startLine = 0;
267 
273  protected $rawLine;
274 
284  protected function readLine() {
285 
286  if (!\is_null($this->lineBuffer)) {
288  $this->lineBuffer = null;
289  } else {
290  do {
291  $eof = \feof($this->input);
292 
293  $rawLine = \fgets($this->input);
294 
295  if ($eof || (\feof($this->input) && $rawLine === false)) {
296  throw new EofException('End of document reached prematurely');
297  }
298  if ($rawLine === false) {
299  throw new ParseException('Error reading from input stream');
300  }
301  $rawLine = \rtrim($rawLine, "\r\n");
302  } while ($rawLine === ''); // Skipping empty lines
303  $this->lineIndex++;
304  }
305  $line = $rawLine;
306 
307  $this->startLine = $this->lineIndex;
308 
309  // Looking ahead for folded lines.
310  while (true) {
311 
312  $nextLine = \rtrim(\fgets($this->input), "\r\n");
313  $this->lineIndex++;
314  if (!$nextLine) {
315  break;
316  }
317  if ($nextLine[0] === "\t" || $nextLine[0] === " ") {
318  $curLine = \substr($nextLine, 1);
319  $line .= $curLine;
320  $rawLine .= "\n " . $curLine;
321  } else {
322  $this->lineBuffer = $nextLine;
323  break;
324  }
325 
326  }
327  $this->rawLine = $rawLine;
328  return $line;
329 
330  }
331 
337  protected function readProperty($line) {
338 
339  if ($this->options & self::OPTION_FORGIVING) {
340  $propNameToken = 'A-Z0-9\-\._\\/';
341  } else {
342  $propNameToken = 'A-Z0-9\-\.';
343  }
344 
345  $paramNameToken = 'A-Z0-9\-';
346  $safeChar = '^";:,';
347  $qSafeChar = '^"';
348 
349  $regex = "/
350  ^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name
351  |
352  (?<=:)(?P<propValue> .+)$ # property value
353  |
354  ;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name
355  |
356  (=|,)(?P<paramValue> # parameter value
357  (?: [$safeChar]*) |
358  \"(?: [$qSafeChar]+)\"
359  ) (?=[;:,])
360  /xi";
361 
362  //echo $regex, "\n"; die();
363  preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
364 
365  $property = [
366  'name' => null,
367  'parameters' => [],
368  'value' => null
369  ];
370 
371  $lastParam = null;
372 
380  foreach ($matches as $match) {
381 
382  if (isset($match['paramValue'])) {
383  if ($match['paramValue'] && $match['paramValue'][0] === '"') {
384  $value = substr($match['paramValue'], 1, -1);
385  } else {
386  $value = $match['paramValue'];
387  }
388 
389  $value = $this->unescapeParam($value);
390 
391  if (is_null($lastParam)) {
392  throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
393  }
394  if (is_null($property['parameters'][$lastParam])) {
395  $property['parameters'][$lastParam] = $value;
396  } elseif (is_array($property['parameters'][$lastParam])) {
397  $property['parameters'][$lastParam][] = $value;
398  } else {
399  $property['parameters'][$lastParam] = [
400  $property['parameters'][$lastParam],
401  $value
402  ];
403  }
404  continue;
405  }
406  if (isset($match['paramName'])) {
407  $lastParam = strtoupper($match['paramName']);
408  if (!isset($property['parameters'][$lastParam])) {
409  $property['parameters'][$lastParam] = null;
410  }
411  continue;
412  }
413  if (isset($match['propValue'])) {
414  $property['value'] = $match['propValue'];
415  continue;
416  }
417  if (isset($match['name']) && $match['name']) {
418  $property['name'] = strtoupper($match['name']);
419  continue;
420  }
421 
422  // @codeCoverageIgnoreStart
423  throw new \LogicException('This code should not be reachable');
424  // @codeCoverageIgnoreEnd
425 
426  }
427 
428  if (is_null($property['value'])) {
429  $property['value'] = '';
430  }
431  if (!$property['name']) {
432  if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
433  return false;
434  }
435  throw new ParseException('Invalid Mimedir file. Line starting at ' . $this->startLine . ' did not follow iCalendar/vCard conventions');
436  }
437 
438  // vCard 2.1 states that parameters may appear without a name, and only
439  // a value. We can deduce the value based on it's name.
440  //
441  // Our parser will get those as parameters without a value instead, so
442  // we're filtering these parameters out first.
443  $namedParameters = [];
444  $namelessParameters = [];
445 
446  foreach ($property['parameters'] as $name => $value) {
447  if (!is_null($value)) {
448  $namedParameters[$name] = $value;
449  } else {
450  $namelessParameters[] = $name;
451  }
452  }
453 
454  $propObj = $this->root->createProperty($property['name'], null, $namedParameters);
455 
456  foreach ($namelessParameters as $namelessParameter) {
457  $propObj->add(null, $namelessParameter);
458  }
459 
460  if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') {
461  $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
462  } else {
464  if ($this->root->getDocumentType() === Document::VCARD21 && isset($propObj['CHARSET'])) {
465  // vCard 2.1 allows the character set to be specified per property.
466  $charset = (string)$propObj['CHARSET'];
467  }
468  switch (strtolower($charset)) {
469  case 'utf-8' :
470  break;
471  case 'iso-8859-1' :
472  $property['value'] = utf8_encode($property['value']);
473  break;
474  case 'windows-1252' :
475  $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
476  break;
477  default :
478  throw new ParseException('Unsupported CHARSET: ' . $propObj['CHARSET']);
479  }
480  $propObj->setRawMimeDirValue($property['value']);
481  }
482 
483  return $propObj;
484 
485  }
486 
549  static function unescapeValue($input, $delimiter = ';') {
550 
551  $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
552  if ($delimiter) {
553  $regex .= ' | (' . $delimiter . ')';
554  }
555  $regex .= ') #x';
556 
557  $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
558 
559  $resultArray = [];
560  $result = '';
561 
562  foreach ($matches as $match) {
563 
564  switch ($match) {
565  case '\\\\' :
566  $result .= '\\';
567  break;
568  case '\N' :
569  case '\n' :
570  $result .= "\n";
571  break;
572  case '\;' :
573  $result .= ';';
574  break;
575  case '\,' :
576  $result .= ',';
577  break;
578  case $delimiter :
579  $resultArray[] = $result;
580  $result = '';
581  break;
582  default :
583  $result .= $match;
584  break;
585 
586  }
587 
588  }
589 
590  $resultArray[] = $result;
591  return $delimiter ? $resultArray : $result;
592 
593  }
594 
629  private function unescapeParam($input) {
630 
631  return
632  preg_replace_callback(
633  '#(\^(\^|n|\'))#',
634  function($matches) {
635  switch ($matches[2]) {
636  case 'n' :
637  return "\n";
638  case '^' :
639  return '^';
640  case '\'' :
641  return '"';
642 
643  // @codeCoverageIgnoreStart
644  }
645  // @codeCoverageIgnoreEnd
646  },
647  $input
648  );
649  }
650 
661  private function extractQuotedPrintableValue() {
662 
663  // We need to parse the raw line again to get the start of the value.
664  //
665  // We are basically looking for the first colon (:), but we need to
666  // skip over the parameters first, as they may contain one.
667  $regex = '/^
668  (?: [^:])+ # Anything but a colon
669  (?: "[^"]")* # A parameter in double quotes
670  : # start of the value we really care about
671  (.*)$
672  /xs';
673 
674  preg_match($regex, $this->rawLine, $matches);
675 
676  $value = $matches[1];
677  // Removing the first whitespace character from every line. Kind of
678  // like unfolding, but we keep the newline.
679  $value = str_replace("\n ", "\n", $value);
680 
681  // Microsoft products don't always correctly fold lines, they may be
682  // missing a whitespace. So if 'forgiving' is turned on, we will take
683  // those as well.
684  if ($this->options & self::OPTION_FORGIVING) {
685  while (substr($value, -1) === '=') {
686  // Reading the line
687  $this->readLine();
688  // Grabbing the raw form
689  $value .= "\n" . $this->rawLine;
690  }
691  }
692 
693  return $value;
694 
695  }
696 
697 }
$lineIndex
The real current line number.
Definition: MimeDir.php:258
setInput($input)
Sets the input buffer.
Definition: MimeDir.php:123
$result
$delimiter
Definition: showstats.php:16
parse($input=null, $options=0)
Parses an iCalendar or vCard file.
Definition: MimeDir.php:77
parseDocument()
Parses an entire document.
Definition: MimeDir.php:148
unescapeParam($input)
Unescapes a parameter value.
Definition: MimeDir.php:629
static $SUPPORTED_CHARSETS
The list of character sets we support when decoding.
Definition: MimeDir.php:60
$stream
PHP stream implementation.
input
Definition: langcheck.php:166
MimeDir parser.
Definition: MimeDir.php:25
Abstract parser.
Definition: Parser.php:14
readProperty($line)
Reads a property or component from a line.
Definition: MimeDir.php:337
setCharset($charset)
By default all input will be assumed to be UTF-8.
Definition: MimeDir.php:107
extractQuotedPrintableValue()
Gets the full quoted printable value.
Definition: MimeDir.php:661
readLine()
Reads a single line from the buffer.
Definition: MimeDir.php:284
parseLine($line)
Parses a line, and if it hits a component, it will also attempt to parse the entire component...
Definition: MimeDir.php:203
Exception thrown by Reader if an invalid object was attempted to be parsed.
const VCARD21
vCard 2.1.
Definition: Document.php:39
Exception thrown by parser when the end of the stream has been reached, before this was expected...
static unescapeValue($input, $delimiter=';')
Unescapes a property value.
Definition: MimeDir.php:549