1<?php
2/**
3 * Copyright 2009-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (BSD). If you
6 * did not receive this file, see http://www.horde.org/licenses/bsd.
7 *
8 * @category  Horde
9 * @copyright 2009-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/bsd BSD
11 * @package   Stream_Wrapper
12 */
13
14/**
15 * A stream wrapper that will combine multiple strings/streams into a single
16 * stream.
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2009-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/bsd BSD
22 * @package   Stream_Wrapper
23 */
24class Horde_Stream_Wrapper_Combine
25{
26    /**/
27    const WRAPPER_NAME = 'horde-stream-wrapper-combine';
28
29    /**
30     * Context.
31     *
32     * @var resource
33     */
34    public $context;
35
36    /**
37     * Array that holds the various streams.
38     *
39     * @var array
40     */
41    protected $_data = array();
42
43    /**
44     * The combined length of the stream.
45     *
46     * @var integer
47     */
48    protected $_length = 0;
49
50    /**
51     * The current position in the string.
52     *
53     * @var integer
54     */
55    protected $_position = 0;
56
57    /**
58     * The current position in the data array.
59     *
60     * @var integer
61     */
62    protected $_datapos = 0;
63
64    /**
65     * Have we reached EOF?
66     *
67     * @var boolean
68     */
69    protected $_ateof = false;
70
71    /**
72     * Unique ID tracker for the streams.
73     *
74     * @var integer
75     */
76    private static $_id = 0;
77
78    /**
79     * Create a stream from multiple data sources.
80     *
81     * @since 2.1.0
82     *
83     * @param array $data  An array of strings and/or streams to combine into
84     *                     a single stream.
85     *
86     * @return resource  A PHP stream.
87     */
88    public static function getStream($data)
89    {
90        if (!self::$_id) {
91            stream_wrapper_register(self::WRAPPER_NAME, __CLASS__);
92        }
93
94        return fopen(
95            self::WRAPPER_NAME . '://' . ++self::$_id,
96            'wb',
97            false,
98            stream_context_create(array(
99                self::WRAPPER_NAME => array(
100                    'data' => $data
101                )
102            ))
103        );
104    }
105    /**
106     * @see streamWrapper::stream_open()
107     *
108     * @param string $path
109     * @param string $mode
110     * @param integer $options
111     * @param string &$opened_path
112     *
113     * @throws Exception
114     */
115    public function stream_open($path, $mode, $options, &$opened_path)
116    {
117        $opts = stream_context_get_options($this->context);
118
119        if (isset($opts[self::WRAPPER_NAME]['data'])) {
120            $data = $opts[self::WRAPPER_NAME]['data'];
121        } elseif (isset($opts['horde-combine']['data'])) {
122            // @deprecated
123            $data = $opts['horde-combine']['data']->getData();
124        } else {
125            throw new Exception('Use ' . __CLASS__ . '::getStream() to initialize the stream.');
126        }
127
128        foreach ($data as $val) {
129            if (is_string($val)) {
130                $fp = fopen('php://temp', 'r+');
131                fwrite($fp, $val);
132            } else {
133                $fp = $val;
134            }
135
136            fseek($fp, 0, SEEK_END);
137            $length = ftell($fp);
138            rewind($fp);
139
140            $this->_data[] = array(
141                'fp' => $fp,
142                'l' => $length,
143                'p' => 0
144            );
145
146            $this->_length += $length;
147        }
148
149        return true;
150    }
151
152    /**
153     * @see streamWrapper::stream_read()
154     *
155     * @param integer $count
156     *
157     * @return mixed
158     */
159    public function stream_read($count)
160    {
161        if ($this->stream_eof()) {
162            return false;
163        }
164
165        $out = '';
166        $tmp = &$this->_data[$this->_datapos];
167
168        while ($count) {
169            if (!is_resource($tmp['fp'])) {
170                return false;
171            }
172
173            $curr_read = min($count, $tmp['l'] - $tmp['p']);
174            $out .= fread($tmp['fp'], $curr_read);
175            $count -= $curr_read;
176            $this->_position += $curr_read;
177
178            if ($this->_position == $this->_length) {
179                if ($count) {
180                    $this->_ateof = true;
181                    break;
182                } else {
183                    $tmp['p'] += $curr_read;
184                }
185            } elseif ($count) {
186                if (!isset($this->_data[++$this->_datapos])) {
187                    return false;
188                }
189                $tmp = &$this->_data[$this->_datapos];
190                rewind($tmp['fp']);
191                $tmp['p'] = 0;
192            } else {
193                $tmp['p'] += $curr_read;
194            }
195        }
196
197        return $out;
198    }
199
200    /**
201     * @see streamWrapper::stream_write()
202     *
203     * @param string $data
204     *
205     * @return integer
206     */
207    public function stream_write($data)
208    {
209        $tmp = &$this->_data[$this->_datapos];
210
211        $oldlen = $tmp['l'];
212        $res = fwrite($tmp['fp'], $data);
213        if ($res === false) {
214            return false;
215        }
216
217        $tmp['p'] = ftell($tmp['fp']);
218        if ($tmp['p'] > $oldlen) {
219            $tmp['l'] = $tmp['p'];
220            $this->_length += ($tmp['l'] - $oldlen);
221        }
222
223        return $res;
224    }
225
226    /**
227     * @see streamWrapper::stream_tell()
228     *
229     * @return integer
230     */
231    public function stream_tell()
232    {
233        return $this->_position;
234    }
235
236    /**
237     * @see streamWrapper::stream_eof()
238     *
239     * @return boolean
240     */
241    public function stream_eof()
242    {
243        return $this->_ateof;
244    }
245
246    /**
247     * @see streamWrapper::stream_stat()
248     *
249     * @return array
250     */
251    public function stream_stat()
252    {
253        return array(
254            'dev' => 0,
255            'ino' => 0,
256            'mode' => 0,
257            'nlink' => 0,
258            'uid' => 0,
259            'gid' => 0,
260            'rdev' => 0,
261            'size' => $this->_length,
262            'atime' => 0,
263            'mtime' => 0,
264            'ctime' => 0,
265            'blksize' => 0,
266            'blocks' => 0
267        );
268    }
269
270    /**
271     * @see streamWrapper::stream_seek()
272     *
273     * @param integer $offset
274     * @param integer $whence  SEEK_SET, SEEK_CUR, or SEEK_END
275     *
276     * @return boolean
277     */
278    public function stream_seek($offset, $whence)
279    {
280        $oldpos = $this->_position;
281        $this->_ateof = false;
282
283        switch ($whence) {
284        case SEEK_SET:
285            $offset = $offset;
286            break;
287
288        case SEEK_CUR:
289            $offset = $this->_position + $offset;
290            break;
291
292        case SEEK_END:
293            $offset = $this->_length + $offset;
294            break;
295
296        default:
297            return false;
298        }
299
300        $count = $this->_position = min($this->_length, $offset);
301
302        foreach ($this->_data as $key => $val) {
303            if ($count < $val['l']) {
304                $this->_datapos = $key;
305                $val['p'] = $count;
306                fseek($val['fp'], $count, SEEK_SET);
307                break;
308            }
309            $count -= $val['l'];
310        }
311
312        return ($oldpos != $this->_position);
313    }
314
315}
316