Passed
Push — master ( 71d0f2...24daad )
by Zaahid
03:42
created

QuotedPrintableStream::read()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 4
nop 1
dl 0
loc 17
ccs 11
cts 12
cp 0.9167
crap 5.0144
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the ZBateson\StreamDecorators project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
namespace ZBateson\StreamDecorators;
8
9
use Psr\Http\Message\StreamInterface;
10
use GuzzleHttp\Psr7\StreamDecoratorTrait;
11
use RuntimeException;
12
13
/**
14
 * GuzzleHttp\Psr7 stream decoder decorator for quoted printable streams.
15
 *
16
 * @author Zaahid Bateson
17
 */
18
class QuotedPrintableStream implements StreamInterface
19
{
20
    use StreamDecoratorTrait;
21
22
    /**
23
     * @var int current read/write position
24
     */
25
    private $position = 0;
26
27
    /**
28
     * @var string Last line of written text (used to maintain good line-breaks)
29
     */
30
    private $lastLine = '';
31
32
    /**
33
     * Overridden to return the position in the target encoding.
34
     *
35
     * @return int
36
     */
37 3
    public function tell()
38
    {
39 3
        return $this->position;
40
    }
41
42
    /**
43
     * Returns null, getSize isn't supported
44
     *
45
     * @return null
46
     */
47 1
    public function getSize()
48
    {
49 1
        return null;
50
    }
51
52
    /**
53
     * Not supported.
54
     *
55
     * @param int $offset
56
     * @param int $whence
57
     * @throws RuntimeException
58
     */
59 1
    public function seek($offset, $whence = SEEK_SET)
60
    {
61 1
        throw new RuntimeException('Cannot seek a QuotedPrintableStream');
62
    }
63
64
    /**
65
     * Overridden to return false
66
     *
67
     * @return boolean
68
     */
69 1
    public function isSeekable()
70
    {
71 1
        return false;
72
    }
73
74
    /**
75
     * Reads $length chars from the underlying stream, prepending the past $pre
76
     * to it first.
77
     *
78
     * If the characters read (including the prepended $pre) contain invalid
79
     * quoted-printable characters, the underlying stream is rewound by the
80
     * total number of characters ($length + strlen($pre)).
81
     *
82
     * The quoted-printable encoded characters are returned.  If the characters
83
     * read are invalid, '3D' is returned indicating an '=' character.
84
     *
85
     * @param int $length
86
     * @param string $pre
87
     * @return string
88
     */
89 5
    private function readEncodedChars($length, $pre = '')
90
    {
91 5
        $str = $pre . $this->stream->read($length);
92 5
        $len = strlen($str);
93 5
        if ($len > 0 && !preg_match('/^[0-9a-f]{2}$|^[\r\n].$/is', $str) && $this->stream->isSeekable()) {
94 1
            $this->stream->seek(-$len, SEEK_CUR);
95 1
            return '3D';    // '=' character
96
        }
97 5
        return $str;
98
    }
99
100
    /**
101
     * Decodes the passed $block of text.
102
     *
103
     * If the last or before last character is an '=' char, indicating the
104
     * beginning of a quoted-printable encoded char, 1 or 2 additional bytes are
105
     * read from the underlying stream respectively.
106
     *
107
     * The decoded string is returned.
108
     *
109
     * @param string $block
110
     * @return string
111
     */
112 8
    private function decodeBlock($block)
113
    {
114 8
        if (substr($block, -1) === '=') {
115 5
            $block .= $this->readEncodedChars(2);
116 8
        } elseif (substr($block, -2, 1) === '=') {
117 4
            $first = substr($block, -1);
118 4
            $block = substr($block, 0, -1);
119 4
            $block .= $this->readEncodedChars(1, $first);
120
        }
121 8
        return quoted_printable_decode($block);
122
    }
123
124
    /**
125
     * Reads up to $length characters, appends them to the passed $str string,
126
     * and returns the total number of characters read.
127
     *
128
     * -1 is returned if there are no more bytes to read.
129
     *
130
     * @param int $length
131
     * @param string $append
132
     * @return int
133
     */
134 8
    private function readRawDecodeAndAppend($length, &$str)
135
    {
136 8
        $block = $this->stream->read($length);
137 8
        if ($block === false || $block === '') {
138 8
            return -1;
139
        }
140 8
        $decoded = $this->decodeBlock($block);
141 8
        $count = strlen($decoded);
142 8
        $str .= $decoded;
143 8
        return $count;
144
    }
145
146
    /**
147
     * Reads up to $length decoded bytes from the underlying quoted-printable
148
     * encoded stream and returns them.
149
     * 
150
     * @param int $length
151
     * @return string
152
     */
153 8
    public function read($length)
154
    {
155
        // let Guzzle decide what to do.
156 8
        if ($length <= 0 || $this->eof()) {
157
            return $this->stream->read($length);
158
        }
159 8
        $count = 0;
160 8
        $bytes = '';
161 8
        while ($count < $length) {
162 8
            $nRead = $this->readRawDecodeAndAppend($length - $count, $bytes);
163 8
            if ($nRead === -1) {
164 8
                break;
165
            }
166 8
            $this->position += $nRead;
167 8
            $count += $nRead;
168
        }
169 8
        return $bytes;
170
    }
171
172
    /**
173
     * Writes the passed string to the underlying stream after encoding it as
174
     * quoted-printable.
175
     *
176
     * Note that reading and writing to the same stream without rewinding is not
177
     * supported.
178
     *
179
     * @param string $string
180
     * @return int the number of bytes written
181
     */
182 1
    public function write($string)
183
    {
184 1
        $encodedLine = quoted_printable_encode($this->lastLine);
185 1
        $lineAndString = rtrim(quoted_printable_encode($this->lastLine . $string), "\r\n");
186 1
        $write = substr($lineAndString, strlen($encodedLine));
187 1
        $this->stream->write($write);
188 1
        $written = strlen($string);
189 1
        $this->position += $written;
190
191 1
        $lpos = strrpos($lineAndString, "\n");
192 1
        $lastLine = $lineAndString;
193 1
        if ($lpos !== false) {
194 1
            $lastLine = substr($lineAndString, $lpos + 1);
195
        }
196 1
        $this->lastLine = quoted_printable_decode($lastLine);
197 1
        return $written;
198
    }
199
200
    /**
201
     * Writes out a final CRLF if the current line isn't empty.
202
     */
203 1
    private function beforeClose()
204
    {
205 1
        if ($this->isWritable() && $this->lastLine !== '') {
206 1
            $this->stream->write("\r\n");
207 1
            $this->lastLine = '';
208
        }
209 1
    }
210
211
    /**
212
     * Closes the underlying stream and writes a final CRLF if the current line
213
     * isn't empty.
214
     */
215 1
    public function close()
216
    {
217 1
        $this->beforeClose();
218 1
        $this->stream->close();
219 1
    }
220
221
    /**
222
     * Closes the underlying stream and writes a final CRLF if the current line
223
     * isn't empty.
224
     */
225
    public function detach()
226
    {
227
        $this->beforeClose();
228
        $this->stream->detach();
229
    }
230
}
231