414 lines
11 KiB
PHP
414 lines
11 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Copyright 2012-2017 Horde LLC (http://www.horde.org/)
|
||
|
*
|
||
|
* See the enclosed file COPYING for license information (LGPL). If you
|
||
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
||
|
*
|
||
|
* @category Horde
|
||
|
* @copyright 2012-2017 Horde LLC
|
||
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
||
|
* @package Imap_Client
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Tokenization of an IMAP data stream.
|
||
|
*
|
||
|
* NOTE: This class is NOT intended to be accessed outside of this package.
|
||
|
* There is NO guarantees that the API of this class will not change across
|
||
|
* versions.
|
||
|
*
|
||
|
* @author Michael Slusarz <slusarz@horde.org>
|
||
|
* @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);
|
||
|
}
|
||
|
|
||
|
}
|