2a2baca2a5a46cdafed34d6c0e52a358c660c1a4
[moodle.git] / lib / horde / framework / Horde / Imap / Client / Tokenize.php
1 <?php
2 /**
3  * Copyright 2012-2017 Horde LLC (http://www.horde.org/)
4  *
5  * See the enclosed file COPYING for license information (LGPL). If you
6  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7  *
8  * @category  Horde
9  * @copyright 2012-2017 Horde LLC
10  * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11  * @package   Imap_Client
12  */
14 /**
15  * Tokenization of an IMAP data stream.
16  *
17  * NOTE: This class is NOT intended to be accessed outside of this package.
18  * There is NO guarantees that the API of this class will not change across
19  * versions.
20  *
21  * @author    Michael Slusarz <slusarz@horde.org>
22  * @category  Horde
23  * @copyright 2012-2017 Horde LLC
24  * @internal
25  * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
26  * @package   Imap_Client
27  *
28  * @property-read boolean $eos  Has the end of the stream been reached?
29  */
30 class Horde_Imap_Client_Tokenize implements Iterator
31 {
32     /**
33      * Current data.
34      *
35      * @var mixed
36      */
37     protected $_current = false;
39     /**
40      * Current key.
41      *
42      * @var integer
43      */
44     protected $_key = false;
46     /**
47      * Sublevel.
48      *
49      * @var integer
50      */
51     protected $_level = false;
53     /**
54      * Array of literal stream objects.
55      *
56      * @var array
57      */
58     protected $_literals = array();
60     /**
61      * Return Horde_Stream object for literal tokens?
62      *
63      * @var boolean
64      */
65     protected $_literalStream = false;
67     /**
68      * next() modifiers.
69      *
70      * @var array
71      */
72     protected $_nextModify = array();
74     /**
75      * Data stream.
76      *
77      * @var Horde_Stream
78      */
79     protected $_stream;
81     /**
82      * Constructor.
83      *
84      * @param mixed $data  Data to add (string, resource, or Horde_Stream
85      *                     object).
86      */
87     public function __construct($data = null)
88     {
89         $this->_stream = new Horde_Stream_Temp();
91         if (!is_null($data)) {
92             $this->add($data);
93         }
94     }
96     /**
97      */
98     public function __clone()
99     {
100         throw new LogicException('Object can not be cloned.');
101     }
103     /**
104      */
105     public function __get($name)
106     {
107         switch ($name) {
108         case 'eos':
109             return $this->_stream->eof();
110         }
111     }
113     /**
114      */
115     public function __sleep()
116     {
117         throw new LogicException('Object can not be serialized.');
118     }
120     /**
121      */
122     public function __toString()
123     {
124         $pos = $this->_stream->pos();
125         $out = $this->_current . ' ' . $this->_stream->getString();
126         $this->_stream->seek($pos, false);
127         return $out;
128     }
130     /**
131      * Add data to buffer.
132      *
133      * @param mixed $data  Data to add (string, resource, or Horde_Stream
134      *                     object).
135      */
136     public function add($data)
137     {
138         $this->_stream->add($data);
139     }
141     /**
142      * Add data to literal stream at the current position.
143      *
144      * @param mixed $data  Data to add (string, resource, or Horde_Stream
145      *                     object).
146      */
147     public function addLiteralStream($data)
148     {
149         $pos = $this->_stream->pos();
150         if (!isset($this->_literals[$pos])) {
151             $this->_literals[$pos] = new Horde_Stream_Temp();
152         }
153         $this->_literals[$pos]->add($data);
154     }
156     /**
157      * Flush the remaining entries left in the iterator.
158      *
159      * @param boolean $return    If true, return entries. Only returns entries
160      *                           on the current level.
161      * @param boolean $sublevel  Only flush items in current sublevel?
162      *
163      * @return array  The entries if $return is true.
164      */
165     public function flushIterator($return = true, $sublevel = true)
166     {
167         $out = array();
169         if ($return) {
170             $this->_nextModify = array(
171                 'level' => $sublevel ? $this->_level : 0,
172                 'out' => array()
173             );
174             $this->next();
175             $out = $this->_nextModify['out'];
176             $this->_nextModify = array();
177         } elseif ($sublevel && $this->_level) {
178             $this->_nextModify = array(
179                 'level' => $this->_level
180             );
181             $this->next();
182             $this->_nextModify = array();
183         } else {
184             $this->_stream->end();
185             $this->_stream->getChar();
186             $this->_current = $this->_key = $this->_level = false;
187         }
189         return $out;
190     }
192     /**
193      * Return literal length data located at the end of the stream.
194      *
195      * @return mixed  Null if no literal data found, or an array with these
196      *                keys:
197      *   - binary: (boolean) True if this is a literal8.
198      *   - length: (integer) Length of the literal.
199      */
200     public function getLiteralLength()
201     {
202         if ($this->_stream->substring(-1, 1) === '}') {
203             $literal_data = $this->_stream->getString(
204                 $this->_stream->search('{', true) - 1
205             );
206             $literal_len = substr($literal_data, 2, -1);
208             if (is_numeric($literal_len)) {
209                 return array(
210                     'binary' => ($literal_data[0] === '~'),
211                     'length' => intval($literal_len)
212                 );
213             }
214         }
216         return null;
217     }
219     /* Iterator methods. */
221     /**
222      */
223     public function current()
224     {
225         return $this->_current;
226     }
228     /**
229      */
230     public function key()
231     {
232         return $this->_key;
233     }
235     /**
236      * @return mixed  Either a string, boolean (true for open paren, false for
237      *                close paren/EOS), Horde_Stream object, or null.
238      */
239     public function next()
240     {
241         $level = isset($this->_nextModify['level'])
242             ? $this->_nextModify['level']
243             : null;
244         /* Directly access stream here to drastically reduce the number of
245          * getChar() calls we would have to make. */
246         $stream = $this->_stream->stream;
248         do {
249             $check_len = true;
250             $in_quote = $text = $binary = false;
252             while (($c = fgetc($stream)) !== false) {
253                 switch ($c) {
254                 case '\\':
255                     $text .= $in_quote
256                         ? fgetc($stream)
257                         : $c;
258                     break;
260                 case '"':
261                     if ($in_quote) {
262                         $check_len = false;
263                         break 2;
264                     }
265                     $in_quote = true;
266                     /* Set $text to non-false (could be empty string). */
267                     $text = '';
268                     break;
270                 default:
271                     if ($in_quote) {
272                         $text .= $c;
273                         break;
274                     }
276                     switch ($c) {
277                     case '(':
278                         ++$this->_level;
279                         $check_len = false;
280                         $text = true;
281                         break 3;
283                     case ')':
284                         if ($text === false) {
285                             --$this->_level;
286                             $check_len = $text = false;
287                         } else {
288                             $this->_stream->seek(-1);
289                         }
290                         break 3;
292                     case '~':
293                         // Ignore binary string identifier. PHP strings are
294                         // binary-safe. But keep it if it is not used as string
295                         // identifier.
296                         $binary = true;
297                         $text .= $c;
298                         continue;
300                     case '{':
301                         if ($binary) {
302                             $text = substr($text, 0, -1);
303                         }
304                         $literal_len = intval($this->_stream->getToChar('}'));
305                         $pos = $this->_stream->pos();
306                         if (isset($this->_literals[$pos])) {
307                             $text = $this->_literals[$pos];
308                             if (!$this->_literalStream) {
309                                 $text = strval($text);
310                             }
311                         } elseif ($this->_literalStream) {
312                             $text = new Horde_Stream_Temp();
313                             while (($literal_len > 0) && !feof($stream)) {
314                                 $part = $this->_stream->substring(
315                                     0,
316                                     min($literal_len, 8192)
317                                 );
318                                 $text->add($part);
319                                 $literal_len -= strlen($part);
320                             }
321                         } else {
322                             $text = $this->_stream->substring(0, $literal_len);
323                         }
324                         $check_len = false;
325                         break 3;
327                     case ' ':
328                         if ($text !== false) {
329                             break 3;
330                         }
331                         break;
333                     default:
334                         $text .= $c;
335                         break;
336                     }
337                     break;
338                 }
339                 $binary = false;
340             }
342             if ($check_len) {
343                 switch (strlen($text)) {
344                 case 0:
345                     $text = false;
346                     break;
348                 case 3:
349                     if (strcasecmp($text, 'NIL') === 0) {
350                         $text = null;
351                     }
352                     break;
353                 }
354             }
356             if (($text === false) && feof($stream)) {
357                 $this->_key = $this->_level = false;
358                 break;
359             }
361             ++$this->_key;
363             if (is_null($level) || ($level > $this->_level)) {
364                 break;
365             }
367             if (($level === $this->_level) && !is_bool($text)) {
368                 $this->_nextModify['out'][] = $text;
369             }
370         } while (true);
372         $this->_current = $text;
374         return $text;
375     }
377     /**
378      * Force return of literal data as stream, if next token.
379      *
380      * @see next()
381      */
382     public function nextStream()
383     {
384         $changed = $this->_literalStream;
385         $this->_literalStream = true;
387         $out = $this->next();
389         if ($changed) {
390             $this->_literalStream = false;
391         }
393         return $out;
394     }
396     /**
397      */
398     public function rewind()
399     {
400         $this->_stream->rewind();
401         $this->_current = false;
402         $this->_key = -1;
403         $this->_level = 0;
404     }
406     /**
407      */
408     public function valid()
409     {
410         return ($this->_level !== false);
411     }