* @category Horde * @copyright 2012-2017 Horde LLC * @internal * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Imap_Client * * @property-read boolean $eos Has the end of the stream been reached? */ class Horde_Imap_Client_Tokenize implements Iterator { /** * Current data. * * @var mixed */ protected $_current = false; /** * Current key. * * @var integer */ protected $_key = false; /** * Sublevel. * * @var integer */ protected $_level = false; /** * Array of literal stream objects. * * @var array */ protected $_literals = array(); /** * Return Horde_Stream object for literal tokens? * * @var boolean */ protected $_literalStream = false; /** * next() modifiers. * * @var array */ protected $_nextModify = array(); /** * Data stream. * * @var Horde_Stream */ protected $_stream; /** * Constructor. * * @param mixed $data Data to add (string, resource, or Horde_Stream * object). */ public function __construct($data = null) { $this->_stream = new Horde_Stream_Temp(); if (!is_null($data)) { $this->add($data); } } /** */ public function __clone() { throw new LogicException('Object can not be cloned.'); } /** */ public function __get($name) { switch ($name) { case 'eos': return $this->_stream->eof(); } } /** */ public function __sleep() { throw new LogicException('Object can not be serialized.'); } /** */ public function __toString() { $pos = $this->_stream->pos(); $out = $this->_current . ' ' . $this->_stream->getString(); $this->_stream->seek($pos, false); return $out; } /** * Add data to buffer. * * @param mixed $data Data to add (string, resource, or Horde_Stream * object). */ public function add($data) { $this->_stream->add($data); } /** * Add data to literal stream at the current position. * * @param mixed $data Data to add (string, resource, or Horde_Stream * object). */ public function addLiteralStream($data) { $pos = $this->_stream->pos(); if (!isset($this->_literals[$pos])) { $this->_literals[$pos] = new Horde_Stream_Temp(); } $this->_literals[$pos]->add($data); } /** * Flush the remaining entries left in the iterator. * * @param boolean $return If true, return entries. Only returns entries * on the current level. * @param boolean $sublevel Only flush items in current sublevel? * * @return array The entries if $return is true. */ public function flushIterator($return = true, $sublevel = true) { $out = array(); if ($return) { $this->_nextModify = array( 'level' => $sublevel ? $this->_level : 0, 'out' => array() ); $this->next(); $out = $this->_nextModify['out']; $this->_nextModify = array(); } elseif ($sublevel && $this->_level) { $this->_nextModify = array( 'level' => $this->_level ); $this->next(); $this->_nextModify = array(); } else { $this->_stream->end(); $this->_stream->getChar(); $this->_current = $this->_key = $this->_level = false; } return $out; } /** * Return literal length data located at the end of the stream. * * @return mixed Null if no literal data found, or an array with these * keys: * - binary: (boolean) True if this is a literal8. * - length: (integer) Length of the literal. */ public function getLiteralLength() { if ($this->_stream->substring(-1, 1) === '}') { $literal_data = $this->_stream->getString( $this->_stream->search('{', true) - 1 ); $literal_len = substr($literal_data, 2, -1); if (is_numeric($literal_len)) { return array( 'binary' => ($literal_data[0] === '~'), 'length' => intval($literal_len) ); } } return null; } /* Iterator methods. */ /** */ public function current() { return $this->_current; } /** */ public function key() { return $this->_key; } /** * @return mixed Either a string, boolean (true for open paren, false for * close paren/EOS), Horde_Stream object, or null. */ public function next() { $level = isset($this->_nextModify['level']) ? $this->_nextModify['level'] : null; /* Directly access stream here to drastically reduce the number of * getChar() calls we would have to make. */ $stream = $this->_stream->stream; do { $check_len = true; $in_quote = $text = $binary = false; while (($c = fgetc($stream)) !== false) { switch ($c) { case '\\': $text .= $in_quote ? fgetc($stream) : $c; break; case '"': if ($in_quote) { $check_len = false; break 2; } $in_quote = true; /* Set $text to non-false (could be empty string). */ $text = ''; break; default: if ($in_quote) { $text .= $c; break; } switch ($c) { case '(': ++$this->_level; $check_len = false; $text = true; break 3; case ')': if ($text === false) { --$this->_level; $check_len = $text = false; } else { $this->_stream->seek(-1); } break 3; case '~': // Ignore binary string identifier. PHP strings are // binary-safe. But keep it if it is not used as string // identifier. $binary = true; $text .= $c; continue 3; case '{': if ($binary) { $text = substr($text, 0, -1); } $literal_len = intval($this->_stream->getToChar('}')); $pos = $this->_stream->pos(); if (isset($this->_literals[$pos])) { $text = $this->_literals[$pos]; if (!$this->_literalStream) { $text = strval($text); } } elseif ($this->_literalStream) { $text = new Horde_Stream_Temp(); while (($literal_len > 0) && !feof($stream)) { $part = $this->_stream->substring( 0, min($literal_len, 8192) ); $text->add($part); $literal_len -= strlen($part); } } else { $text = $this->_stream->substring(0, $literal_len); } $check_len = false; break 3; case ' ': if ($text !== false) { break 3; } break; default: $text .= $c; break; } break; } $binary = false; } if ($check_len) { switch (strlen($text)) { case 0: $text = false; break; case 3: if (strcasecmp($text, 'NIL') === 0) { $text = null; } break; } } if (($text === false) && feof($stream)) { $this->_key = $this->_level = false; break; } ++$this->_key; if (is_null($level) || ($level > $this->_level)) { break; } if (($level === $this->_level) && !is_bool($text)) { $this->_nextModify['out'][] = $text; } } while (true); $this->_current = $text; return $text; } /** * Force return of literal data as stream, if next token. * * @see next() */ public function nextStream() { $changed = $this->_literalStream; $this->_literalStream = true; $out = $this->next(); if ($changed) { $this->_literalStream = false; } return $out; } /** */ public function rewind() { $this->_stream->rewind(); $this->_current = false; $this->_key = -1; $this->_level = 0; } /** */ public function valid() { return ($this->_level !== false); } }