xref: /plugin/davcal/vendor/sabre/vobject/lib/Parser/MimeDir.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
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