PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / sabre / vobject / lib / Parser / MimeDir.php
ameliabooking / vendor / sabre / vobject / lib / Parser Last commit date
XML 7 months ago Json.php 7 months ago MimeDir.php 7 months ago Parser.php 7 months ago XML.php 7 months ago
MimeDir.php
690 lines
1 <?php
2
3 namespace AmeliaVendor\Sabre\VObject\Parser;
4
5 use AmeliaVendor\Sabre\VObject\Component;
6 use AmeliaVendor\Sabre\VObject\Component\VCalendar;
7 use AmeliaVendor\Sabre\VObject\Component\VCard;
8 use AmeliaVendor\Sabre\VObject\Document;
9 use AmeliaVendor\Sabre\VObject\EofException;
10 use AmeliaVendor\Sabre\VObject\Node;
11 use AmeliaVendor\Sabre\VObject\ParseException;
12
13 /**
14 * MimeDir parser.
15 *
16 * This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This
17 * parser will return one of the following two objects from the parse method:
18 *
19 * Sabre\VObject\Component\VCalendar
20 * Sabre\VObject\Component\VCard
21 *
22 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
23 * @author Evert Pot (http://evertpot.com/)
24 * @license http://sabre.io/license/ Modified BSD License
25 */
26 class MimeDir extends Parser
27 {
28 /**
29 * The input stream.
30 *
31 * @var resource
32 */
33 protected $input;
34
35 /**
36 * Root component.
37 *
38 * @var Component
39 */
40 protected $root;
41
42 /**
43 * By default all input will be assumed to be UTF-8.
44 *
45 * However, both iCalendar and vCard might be encoded using different
46 * character sets. The character set is usually set in the mime-type.
47 *
48 * If this is the case, use setEncoding to specify that a different
49 * encoding will be used. If this is set, the parser will automatically
50 * convert all incoming data to UTF-8.
51 *
52 * @var string
53 */
54 protected $charset = 'UTF-8';
55
56 /**
57 * The list of character sets we support when decoding.
58 *
59 * This would be a const expression but for now we need to support PHP 5.5
60 */
61 protected static $SUPPORTED_CHARSETS = [
62 'UTF-8',
63 'ISO-8859-1',
64 'Windows-1252',
65 ];
66
67 /**
68 * Parses an iCalendar or vCard file.
69 *
70 * Pass a stream or a string. If null is parsed, the existing buffer is
71 * used.
72 *
73 * @param string|resource|null $input
74 * @param int $options
75 *
76 * @return \AmeliaVendor\Sabre\VObject\Document
77 */
78 public function parse($input = null, $options = 0)
79 {
80 $this->root = null;
81
82 if (!is_null($input)) {
83 $this->setInput($input);
84 }
85
86 if (!\is_resource($this->input)) {
87 // Null was passed as input, but there was no existing input buffer
88 // There is nothing to parse.
89 throw new ParseException('No input provided to parse');
90 }
91
92 if (0 !== $options) {
93 $this->options = $options;
94 }
95
96 $this->parseDocument();
97
98 return $this->root;
99 }
100
101 /**
102 * By default all input will be assumed to be UTF-8.
103 *
104 * However, both iCalendar and vCard might be encoded using different
105 * character sets. The character set is usually set in the mime-type.
106 *
107 * If this is the case, use setEncoding to specify that a different
108 * encoding will be used. If this is set, the parser will automatically
109 * convert all incoming data to UTF-8.
110 *
111 * @param string $charset
112 */
113 public function setCharset($charset)
114 {
115 if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
116 throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
117 }
118 $this->charset = $charset;
119 }
120
121 /**
122 * Sets the input buffer. Must be a string or stream.
123 *
124 * @param resource|string $input
125 */
126 public function setInput($input)
127 {
128 // Resetting the parser
129 $this->lineIndex = 0;
130 $this->startLine = 0;
131
132 if (is_string($input)) {
133 // Converting to a stream.
134 $stream = fopen('php://temp', 'r+');
135 fwrite($stream, $input);
136 rewind($stream);
137 $this->input = $stream;
138 } elseif (is_resource($input)) {
139 $this->input = $input;
140 } else {
141 throw new \InvalidArgumentException('This parser can only read from strings or streams.');
142 }
143 }
144
145 /**
146 * Parses an entire document.
147 */
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 && 0xef === ord($line[0])
156 && 0xbb === ord($line[1])
157 && 0xbf === ord($line[2])) {
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 // Reading until we hit END:
176 try {
177 $line = $this->readLine();
178 } catch (EofException $oEx) {
179 $line = 'END:'.$this->root->name;
180 }
181 if ('END:' === strtoupper(substr($line, 0, 4))) {
182 break;
183 }
184 $result = $this->parseLine($line);
185 if ($result) {
186 $this->root->add($result);
187 }
188 }
189
190 $name = strtoupper(substr($line, 4));
191 if ($name !== $this->root->name) {
192 throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"');
193 }
194 }
195
196 /**
197 * Parses a line, and if it hits a component, it will also attempt to parse
198 * the entire component.
199 *
200 * @param string $line Unfolded line
201 *
202 * @return Node
203 */
204 protected function parseLine($line)
205 {
206 // Start of a new component
207 if ('BEGIN:' === strtoupper(substr($line, 0, 6))) {
208 if (substr($line, 6) === $this->root->name) {
209 throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name);
210 }
211 $component = $this->root->createComponent(substr($line, 6), [], false);
212
213 while (true) {
214 // Reading until we hit END:
215 $line = $this->readLine();
216 if ('END:' === strtoupper(substr($line, 0, 4))) {
217 break;
218 }
219 $result = $this->parseLine($line);
220 if ($result) {
221 $component->add($result);
222 }
223 }
224
225 $name = strtoupper(substr($line, 4));
226 if ($name !== $component->name) {
227 throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"');
228 }
229
230 return $component;
231 } else {
232 // Property reader
233 $property = $this->readProperty($line);
234 if (!$property) {
235 // Ignored line
236 return false;
237 }
238
239 return $property;
240 }
241 }
242
243 /**
244 * We need to look ahead 1 line every time to see if we need to 'unfold'
245 * the next line.
246 *
247 * If that was not the case, we store it here.
248 *
249 * @var string|null
250 */
251 protected $lineBuffer;
252
253 /**
254 * The real current line number.
255 */
256 protected $lineIndex = 0;
257
258 /**
259 * In the case of unfolded lines, this property holds the line number for
260 * the start of the line.
261 *
262 * @var int
263 */
264 protected $startLine = 0;
265
266 /**
267 * Contains a 'raw' representation of the current line.
268 *
269 * @var string
270 */
271 protected $rawLine;
272
273 /**
274 * Reads a single line from the buffer.
275 *
276 * This method strips any newlines and also takes care of unfolding.
277 *
278 * @throws \AmeliaVendor\Sabre\VObject\EofException
279 *
280 * @return string
281 */
282 protected function readLine()
283 {
284 if (!\is_null($this->lineBuffer)) {
285 $rawLine = $this->lineBuffer;
286 $this->lineBuffer = null;
287 } else {
288 do {
289 $eof = \feof($this->input);
290
291 $rawLine = \fgets($this->input);
292
293 if ($eof || (\feof($this->input) && false === $rawLine)) {
294 throw new EofException('End of document reached prematurely');
295 }
296 if (false === $rawLine) {
297 throw new ParseException('Error reading from input stream');
298 }
299 $rawLine = \rtrim($rawLine, "\r\n");
300 } while ('' === $rawLine); // Skipping empty lines
301 ++$this->lineIndex;
302 }
303 $line = $rawLine;
304
305 $this->startLine = $this->lineIndex;
306
307 // Looking ahead for folded lines.
308 while (true) {
309 $nextLine = \rtrim(\fgets($this->input), "\r\n");
310 ++$this->lineIndex;
311 if (!$nextLine) {
312 break;
313 }
314 if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) {
315 $curLine = \substr($nextLine, 1);
316 $line .= $curLine;
317 $rawLine .= "\n ".$curLine;
318 } else {
319 $this->lineBuffer = $nextLine;
320 break;
321 }
322 }
323 $this->rawLine = $rawLine;
324
325 return $line;
326 }
327
328 /**
329 * Reads a property or component from a line.
330 */
331 protected function readProperty($line)
332 {
333 if ($this->options & self::OPTION_FORGIVING) {
334 $propNameToken = 'A-Z0-9\-\._\\/';
335 } else {
336 $propNameToken = 'A-Z0-9\-\.';
337 }
338
339 $paramNameToken = 'A-Z0-9\-';
340 $safeChar = '^";:,';
341 $qSafeChar = '^"';
342
343 $regex = "/
344 ^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name
345 |
346 (?<=:)(?P<propValue> .+)$ # property value
347 |
348 ;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name
349 |
350 (=|,)(?P<paramValue> # parameter value
351 (?: [$safeChar]*) |
352 \"(?: [$qSafeChar]+)\"
353 ) (?=[;:,])
354 /xi";
355
356 //echo $regex, "\n"; exit();
357 preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
358
359 $property = [
360 'name' => null,
361 'parameters' => [],
362 'value' => null,
363 ];
364
365 $lastParam = null;
366
367 /*
368 * Looping through all the tokens.
369 *
370 * Note that we are looping through them in reverse order, because if a
371 * sub-pattern matched, the subsequent named patterns will not show up
372 * in the result.
373 */
374 foreach ($matches as $match) {
375 if (isset($match['paramValue'])) {
376 if ($match['paramValue'] && '"' === $match['paramValue'][0]) {
377 $value = substr($match['paramValue'], 1, -1);
378 } else {
379 $value = $match['paramValue'];
380 }
381
382 $value = $this->unescapeParam($value);
383
384 if (is_null($lastParam)) {
385 if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
386 // When the property can't be matched and the configuration
387 // option is set to ignore invalid lines, we ignore this line
388 // This can happen when servers provide faulty data as iCloud
389 // frequently does with X-APPLE-STRUCTURED-LOCATION
390 continue;
391 }
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 } elseif ($property['parameters'][$lastParam] === $value) {
399 // When the current value of the parameter is the same as the
400 // new one, then we can leave the current parameter as it is.
401 } else {
402 $property['parameters'][$lastParam] = [
403 $property['parameters'][$lastParam],
404 $value,
405 ];
406 }
407 continue;
408 }
409 if (isset($match['paramName'])) {
410 $lastParam = strtoupper($match['paramName']);
411 if (!isset($property['parameters'][$lastParam])) {
412 $property['parameters'][$lastParam] = null;
413 }
414 continue;
415 }
416 if (isset($match['propValue'])) {
417 $property['value'] = $match['propValue'];
418 continue;
419 }
420 if (isset($match['name']) && $match['name']) {
421 $property['name'] = strtoupper($match['name']);
422 continue;
423 }
424
425 // @codeCoverageIgnoreStart
426 throw new \LogicException('This code should not be reachable');
427 // @codeCoverageIgnoreEnd
428 }
429
430 if (is_null($property['value'])) {
431 $property['value'] = '';
432 }
433 if (!$property['name']) {
434 if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
435 return false;
436 }
437 throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
438 }
439
440 // vCard 2.1 states that parameters may appear without a name, and only
441 // a value. We can deduce the value based on its name.
442 //
443 // Our parser will get those as parameters without a value instead, so
444 // we're filtering these parameters out first.
445 $namedParameters = [];
446 $namelessParameters = [];
447
448 foreach ($property['parameters'] as $name => $value) {
449 if (!is_null($value)) {
450 $namedParameters[$name] = $value;
451 } else {
452 $namelessParameters[] = $name;
453 }
454 }
455
456 $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line);
457
458 foreach ($namelessParameters as $namelessParameter) {
459 $propObj->add(null, $namelessParameter);
460 }
461
462 if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) {
463 $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
464 } else {
465 $charset = $this->charset;
466 if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) {
467 // vCard 2.1 allows the character set to be specified per property.
468 $charset = (string) $propObj['CHARSET'];
469 }
470 switch (strtolower($charset)) {
471 case 'utf-8':
472 break;
473 case 'windows-1252':
474 case 'iso-8859-1':
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 /**
487 * Unescapes a property value.
488 *
489 * vCard 2.1 says:
490 * * Semi-colons must be escaped in some property values, specifically
491 * ADR, ORG and N.
492 * * Semi-colons must be escaped in parameter values, because semi-colons
493 * are also use to separate values.
494 * * No mention of escaping backslashes with another backslash.
495 * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
496 * span values over more than 1 line.
497 *
498 * vCard 3.0 says:
499 * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
500 * escaped, all time time.
501 * * Comma's are used for delimiters in multiple values
502 * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
503 * as in some properties semi-colon is used for separators.
504 * * Properties using semi-colons: N, ADR, GEO, ORG
505 * * Both ADR and N's individual parts may be broken up further with a
506 * comma.
507 * * Properties using commas: NICKNAME, CATEGORIES
508 *
509 * vCard 4.0 (rfc6350) says:
510 * * Commas must be escaped.
511 * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
512 * delimiter, depending on the property.
513 * * Backslashes must be escaped
514 * * Newlines must be escaped as either \N or \n.
515 * * Some compound properties may contain multiple parts themselves, so a
516 * comma within a semi-colon delimited property may also be unescaped
517 * to denote multiple parts _within_ the compound property.
518 * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
519 * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
520 *
521 * Even though the spec says that commas must always be escaped, the
522 * example for GEO in Section 6.5.2 seems to violate this.
523 *
524 * iCalendar 2.0 (rfc5545) says:
525 * * Commas or semi-colons may be used as delimiters, depending on the
526 * property.
527 * * Commas, semi-colons, backslashes, newline (\N or \n) are always
528 * escaped, unless they are delimiters.
529 * * Colons shall not be escaped.
530 * * Commas can be considered the 'default delimiter' and is described as
531 * the delimiter in cases where the order of the multiple values is
532 * insignificant.
533 * * Semi-colons are described as the delimiter for 'structured values'.
534 * They are specifically used in Semi-colons are used as a delimiter in
535 * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
536 *
537 * Now for the parameters
538 *
539 * If delimiter is not set (empty string) this method will just return a string.
540 * If it's a comma or a semi-colon the string will be split on those
541 * characters, and always return an array.
542 *
543 * @param string $input
544 * @param string $delimiter
545 *
546 * @return string|string[]
547 */
548 public static function unescapeValue($input, $delimiter = ';')
549 {
550 $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
551 if ($delimiter) {
552 $regex .= ' | ('.$delimiter.')';
553 }
554 $regex .= ') #x';
555
556 $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
557
558 $resultArray = [];
559 $result = '';
560
561 foreach ($matches as $match) {
562 switch ($match) {
563 case '\\\\':
564 $result .= '\\';
565 break;
566 case '\N':
567 case '\n':
568 $result .= "\n";
569 break;
570 case '\;':
571 $result .= ';';
572 break;
573 case '\,':
574 $result .= ',';
575 break;
576 case $delimiter:
577 $resultArray[] = $result;
578 $result = '';
579 break;
580 default:
581 $result .= $match;
582 break;
583 }
584 }
585
586 $resultArray[] = $result;
587
588 return $delimiter ? $resultArray : $result;
589 }
590
591 /**
592 * Unescapes a parameter value.
593 *
594 * vCard 2.1:
595 * * Does not mention a mechanism for this. In addition, double quotes
596 * are never used to wrap values.
597 * * This means that parameters can simply not contain colons or
598 * semi-colons.
599 *
600 * vCard 3.0 (rfc2425, rfc2426):
601 * * Parameters _may_ be surrounded by double quotes.
602 * * If this is not the case, semi-colon, colon and comma may simply not
603 * occur (the comma used for multiple parameter values though).
604 * * If it is surrounded by double-quotes, it may simply not contain
605 * double-quotes.
606 * * This means that a parameter can in no case encode double-quotes, or
607 * newlines.
608 *
609 * vCard 4.0 (rfc6350)
610 * * Behavior seems to be identical to vCard 3.0
611 *
612 * iCalendar 2.0 (rfc5545)
613 * * Behavior seems to be identical to vCard 3.0
614 *
615 * Parameter escaping mechanism (rfc6868) :
616 * * This rfc describes a new way to escape parameter values.
617 * * New-line is encoded as ^n
618 * * ^ is encoded as ^^.
619 * * " is encoded as ^'
620 *
621 * @param string $input
622 */
623 private function unescapeParam($input)
624 {
625 return
626 preg_replace_callback(
627 '#(\^(\^|n|\'))#',
628 function ($matches) {
629 switch ($matches[2]) {
630 case 'n':
631 return "\n";
632 case '^':
633 return '^';
634 case '\'':
635 return '"';
636
637 // @codeCoverageIgnoreStart
638 }
639 // @codeCoverageIgnoreEnd
640 },
641 $input
642 );
643 }
644
645 /**
646 * Gets the full quoted printable value.
647 *
648 * We need a special method for this, because newlines have both a meaning
649 * in vCards, and in QuotedPrintable.
650 *
651 * This method does not do any decoding.
652 *
653 * @return string
654 */
655 private function extractQuotedPrintableValue()
656 {
657 // We need to parse the raw line again to get the start of the value.
658 //
659 // We are basically looking for the first colon (:), but we need to
660 // skip over the parameters first, as they may contain one.
661 $regex = '/^
662 (?: [^:])+ # Anything but a colon
663 (?: "[^"]")* # A parameter in double quotes
664 : # start of the value we really care about
665 (.*)$
666 /xs';
667
668 preg_match($regex, $this->rawLine, $matches);
669
670 $value = $matches[1];
671 // Removing the first whitespace character from every line. Kind of
672 // like unfolding, but we keep the newline.
673 $value = str_replace("\n ", "\n", $value);
674
675 // Microsoft products don't always correctly fold lines, they may be
676 // missing a whitespace. So if 'forgiving' is turned on, we will take
677 // those as well.
678 if ($this->options & self::OPTION_FORGIVING) {
679 while ('=' === substr($value, -1) && $this->lineBuffer) {
680 // Reading the line
681 $this->readLine();
682 // Grabbing the raw form
683 $value .= "\n".$this->rawLine;
684 }
685 }
686
687 return $value;
688 }
689 }
690