1<?php
2/*
3 * Copyright 2016-2017 MongoDB, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *   http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace MongoDB\GridFS;
19
20use Exception;
21use MongoDB\BSON\UTCDateTime;
22use stdClass;
23use function explode;
24use function get_class;
25use function in_array;
26use function is_integer;
27use function sprintf;
28use function stream_context_get_options;
29use function stream_get_wrappers;
30use function stream_wrapper_register;
31use function stream_wrapper_unregister;
32use function trigger_error;
33use const E_USER_WARNING;
34use const SEEK_CUR;
35use const SEEK_END;
36use const SEEK_SET;
37use const STREAM_IS_URL;
38
39/**
40 * Stream wrapper for reading and writing a GridFS file.
41 *
42 * @internal
43 * @see Bucket::openUploadStream()
44 * @see Bucket::openDownloadStream()
45 */
46class StreamWrapper
47{
48    /** @var resource|null Stream context (set by PHP) */
49    public $context;
50
51    /** @var string|null */
52    private $mode;
53
54    /** @var string|null */
55    private $protocol;
56
57    /** @var ReadableStream|WritableStream|null */
58    private $stream;
59
60    /**
61     * Return the stream's file document.
62     *
63     * @return stdClass
64     */
65    public function getFile()
66    {
67        return $this->stream->getFile();
68    }
69
70    /**
71     * Register the GridFS stream wrapper.
72     *
73     * @param string $protocol Protocol to use for stream_wrapper_register()
74     */
75    public static function register($protocol = 'gridfs')
76    {
77        if (in_array($protocol, stream_get_wrappers())) {
78            stream_wrapper_unregister($protocol);
79        }
80
81        stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
82    }
83
84    /**
85     * Closes the stream.
86     *
87     * @see http://php.net/manual/en/streamwrapper.stream-close.php
88     */
89    public function stream_close()
90    {
91        $this->stream->close();
92    }
93
94    /**
95     * Returns whether the file pointer is at the end of the stream.
96     *
97     * @see http://php.net/manual/en/streamwrapper.stream-eof.php
98     * @return boolean
99     */
100    public function stream_eof()
101    {
102        if (! $this->stream instanceof ReadableStream) {
103            return false;
104        }
105
106        return $this->stream->isEOF();
107    }
108
109    /**
110     * Opens the stream.
111     *
112     * @see http://php.net/manual/en/streamwrapper.stream-open.php
113     * @param string  $path       Path to the file resource
114     * @param string  $mode       Mode used to open the file (only "r" and "w" are supported)
115     * @param integer $options    Additional flags set by the streams API
116     * @param string  $openedPath Not used
117     * @return boolean
118     */
119    public function stream_open($path, $mode, $options, &$openedPath)
120    {
121        $this->initProtocol($path);
122        $this->mode = $mode;
123
124        if ($mode === 'r') {
125            return $this->initReadableStream();
126        }
127
128        if ($mode === 'w') {
129            return $this->initWritableStream();
130        }
131
132        return false;
133    }
134
135    /**
136     * Read bytes from the stream.
137     *
138     * Note: this method may return a string smaller than the requested length
139     * if data is not available to be read.
140     *
141     * @see http://php.net/manual/en/streamwrapper.stream-read.php
142     * @param integer $length Number of bytes to read
143     * @return string
144     */
145    public function stream_read($length)
146    {
147        if (! $this->stream instanceof ReadableStream) {
148            return '';
149        }
150
151        try {
152            return $this->stream->readBytes($length);
153        } catch (Exception $e) {
154            trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), E_USER_WARNING);
155
156            return false;
157        }
158    }
159
160    /**
161     * Return the current position of the stream.
162     *
163     * @see http://php.net/manual/en/streamwrapper.stream-seek.php
164     * @param integer $offset Stream offset to seek to
165     * @param integer $whence One of SEEK_SET, SEEK_CUR, or SEEK_END
166     * @return boolean True if the position was updated and false otherwise
167     */
168    public function stream_seek($offset, $whence = SEEK_SET)
169    {
170        $size = $this->stream->getSize();
171
172        if ($whence === SEEK_CUR) {
173            $offset += $this->stream->tell();
174        }
175
176        if ($whence === SEEK_END) {
177            $offset += $size;
178        }
179
180        // WritableStreams are always positioned at the end of the stream
181        if ($this->stream instanceof WritableStream) {
182            return $offset === $size;
183        }
184
185        if ($offset < 0 || $offset > $size) {
186            return false;
187        }
188
189        $this->stream->seek($offset);
190
191        return true;
192    }
193
194    /**
195     * Return information about the stream.
196     *
197     * @see http://php.net/manual/en/streamwrapper.stream-stat.php
198     * @return array
199     */
200    public function stream_stat()
201    {
202        $stat = $this->getStatTemplate();
203
204        $stat[2] = $stat['mode'] = $this->stream instanceof ReadableStream
205            ? 0100444  // S_IFREG & S_IRUSR & S_IRGRP & S_IROTH
206            : 0100222; // S_IFREG & S_IWUSR & S_IWGRP & S_IWOTH
207        $stat[7] = $stat['size'] = $this->stream->getSize();
208
209        $file = $this->stream->getFile();
210
211        if (isset($file->uploadDate) && $file->uploadDate instanceof UTCDateTime) {
212            $timestamp = $file->uploadDate->toDateTime()->getTimestamp();
213            $stat[9] = $stat['mtime'] = $timestamp;
214            $stat[10] = $stat['ctime'] = $timestamp;
215        }
216
217        if (isset($file->chunkSize) && is_integer($file->chunkSize)) {
218            $stat[11] = $stat['blksize'] = $file->chunkSize;
219        }
220
221        return $stat;
222    }
223
224    /**
225     * Return the current position of the stream.
226     *
227     * @see http://php.net/manual/en/streamwrapper.stream-tell.php
228     * @return integer The current position of the stream
229     */
230    public function stream_tell()
231    {
232        return $this->stream->tell();
233    }
234
235    /**
236     * Write bytes to the stream.
237     *
238     * @see http://php.net/manual/en/streamwrapper.stream-write.php
239     * @param string $data Data to write
240     * @return integer The number of bytes written
241     */
242    public function stream_write($data)
243    {
244        if (! $this->stream instanceof WritableStream) {
245            return 0;
246        }
247
248        try {
249            return $this->stream->writeBytes($data);
250        } catch (Exception $e) {
251            trigger_error(sprintf('%s: %s', get_class($e), $e->getMessage()), E_USER_WARNING);
252
253            return false;
254        }
255    }
256
257    /**
258     * Returns a stat template with default values.
259     *
260     * @return array
261     */
262    private function getStatTemplate()
263    {
264        return [
265            // phpcs:disable Squiz.Arrays.ArrayDeclaration.IndexNoNewline
266            0  => 0,  'dev'     => 0,
267            1  => 0,  'ino'     => 0,
268            2  => 0,  'mode'    => 0,
269            3  => 0,  'nlink'   => 0,
270            4  => 0,  'uid'     => 0,
271            5  => 0,  'gid'     => 0,
272            6  => -1, 'rdev'    => -1,
273            7  => 0,  'size'    => 0,
274            8  => 0,  'atime'   => 0,
275            9  => 0,  'mtime'   => 0,
276            10 => 0,  'ctime'   => 0,
277            11 => -1, 'blksize' => -1,
278            12 => -1, 'blocks'  => -1,
279            // phpcs:enable
280        ];
281    }
282
283    /**
284     * Initialize the protocol from the given path.
285     *
286     * @see StreamWrapper::stream_open()
287     * @param string $path
288     */
289    private function initProtocol($path)
290    {
291        $parts = explode('://', $path, 2);
292        $this->protocol = $parts[0] ?: 'gridfs';
293    }
294
295    /**
296     * Initialize the internal stream for reading.
297     *
298     * @see StreamWrapper::stream_open()
299     * @return boolean
300     */
301    private function initReadableStream()
302    {
303        $context = stream_context_get_options($this->context);
304
305        $this->stream = new ReadableStream(
306            $context[$this->protocol]['collectionWrapper'],
307            $context[$this->protocol]['file']
308        );
309
310        return true;
311    }
312
313    /**
314     * Initialize the internal stream for writing.
315     *
316     * @see StreamWrapper::stream_open()
317     * @return boolean
318     */
319    private function initWritableStream()
320    {
321        $context = stream_context_get_options($this->context);
322
323        $this->stream = new WritableStream(
324            $context[$this->protocol]['collectionWrapper'],
325            $context[$this->protocol]['filename'],
326            $context[$this->protocol]['options']
327        );
328
329        return true;
330    }
331}
332