1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Allows reading and writing of bytes to and from a file.
13 *
14 * @author Chris Corbyn
15 */
16class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream implements Swift_FileStream
17{
18    /** The internal pointer offset */
19    private $_offset = 0;
20
21    /** The path to the file */
22    private $_path;
23
24    /** The mode this file is opened in for writing */
25    private $_mode;
26
27    /** A lazy-loaded resource handle for reading the file */
28    private $_reader;
29
30    /** A lazy-loaded resource handle for writing the file */
31    private $_writer;
32
33    /** If magic_quotes_runtime is on, this will be true */
34    private $_quotes = false;
35
36    /** If stream is seekable true/false, or null if not known */
37    private $_seekable = null;
38
39    /**
40     * Create a new FileByteStream for $path.
41     *
42     * @param string $path
43     * @param bool   $writable if true
44     */
45    public function __construct($path, $writable = false)
46    {
47        if (empty($path)) {
48            throw new Swift_IoException('The path cannot be empty');
49        }
50        $this->_path = $path;
51        $this->_mode = $writable ? 'w+b' : 'rb';
52
53        if (function_exists('get_magic_quotes_runtime') && @get_magic_quotes_runtime() == 1) {
54            $this->_quotes = true;
55        }
56    }
57
58    /**
59     * Get the complete path to the file.
60     *
61     * @return string
62     */
63    public function getPath()
64    {
65        return $this->_path;
66    }
67
68    /**
69     * Reads $length bytes from the stream into a string and moves the pointer
70     * through the stream by $length.
71     *
72     * If less bytes exist than are requested the
73     * remaining bytes are given instead. If no bytes are remaining at all, boolean
74     * false is returned.
75     *
76     * @param int $length
77     *
78     * @throws Swift_IoException
79     *
80     * @return string|bool
81     */
82    public function read($length)
83    {
84        $fp = $this->_getReadHandle();
85        if (!feof($fp)) {
86            if ($this->_quotes) {
87                ini_set('magic_quotes_runtime', 0);
88            }
89            $bytes = fread($fp, $length);
90            if ($this->_quotes) {
91                ini_set('magic_quotes_runtime', 1);
92            }
93            $this->_offset = ftell($fp);
94
95            // If we read one byte after reaching the end of the file
96            // feof() will return false and an empty string is returned
97            if ($bytes === '' && feof($fp)) {
98                $this->_resetReadHandle();
99
100                return false;
101            }
102
103            return $bytes;
104        }
105
106        $this->_resetReadHandle();
107
108        return false;
109    }
110
111    /**
112     * Move the internal read pointer to $byteOffset in the stream.
113     *
114     * @param int $byteOffset
115     *
116     * @return bool
117     */
118    public function setReadPointer($byteOffset)
119    {
120        if (isset($this->_reader)) {
121            $this->_seekReadStreamToPosition($byteOffset);
122        }
123        $this->_offset = $byteOffset;
124    }
125
126    /** Just write the bytes to the file */
127    protected function _commit($bytes)
128    {
129        fwrite($this->_getWriteHandle(), $bytes);
130        $this->_resetReadHandle();
131    }
132
133    /** Not used */
134    protected function _flush()
135    {
136    }
137
138    /** Get the resource for reading */
139    private function _getReadHandle()
140    {
141        if (!isset($this->_reader)) {
142            $pointer = @fopen($this->_path, 'rb');
143            if (!$pointer) {
144                throw new Swift_IoException(
145                    'Unable to open file for reading ['.$this->_path.']'
146                );
147            }
148            $this->_reader = $pointer;
149            if ($this->_offset != 0) {
150                $this->_getReadStreamSeekableStatus();
151                $this->_seekReadStreamToPosition($this->_offset);
152            }
153        }
154
155        return $this->_reader;
156    }
157
158    /** Get the resource for writing */
159    private function _getWriteHandle()
160    {
161        if (!isset($this->_writer)) {
162            if (!$this->_writer = fopen($this->_path, $this->_mode)) {
163                throw new Swift_IoException(
164                    'Unable to open file for writing ['.$this->_path.']'
165                );
166            }
167        }
168
169        return $this->_writer;
170    }
171
172    /** Force a reload of the resource for reading */
173    private function _resetReadHandle()
174    {
175        if (isset($this->_reader)) {
176            fclose($this->_reader);
177            $this->_reader = null;
178        }
179    }
180
181    /** Check if ReadOnly Stream is seekable */
182    private function _getReadStreamSeekableStatus()
183    {
184        $metas = stream_get_meta_data($this->_reader);
185        $this->_seekable = $metas['seekable'];
186    }
187
188    /** Streams in a readOnly stream ensuring copy if needed */
189    private function _seekReadStreamToPosition($offset)
190    {
191        if ($this->_seekable === null) {
192            $this->_getReadStreamSeekableStatus();
193        }
194        if ($this->_seekable === false) {
195            $currentPos = ftell($this->_reader);
196            if ($currentPos < $offset) {
197                $toDiscard = $offset - $currentPos;
198                fread($this->_reader, $toDiscard);
199
200                return;
201            }
202            $this->_copyReadStream();
203        }
204        fseek($this->_reader, $offset, SEEK_SET);
205    }
206
207    /** Copy a readOnly Stream to ensure seekability */
208    private function _copyReadStream()
209    {
210        if ($tmpFile = fopen('php://temp/maxmemory:4096', 'w+b')) {
211            /* We have opened a php:// Stream Should work without problem */
212        } elseif (function_exists('sys_get_temp_dir') && is_writable(sys_get_temp_dir()) && ($tmpFile = tmpfile())) {
213            /* We have opened a tmpfile */
214        } else {
215            throw new Swift_IoException('Unable to copy the file to make it seekable, sys_temp_dir is not writable, php://memory not available');
216        }
217        $currentPos = ftell($this->_reader);
218        fclose($this->_reader);
219        $source = fopen($this->_path, 'rb');
220        if (!$source) {
221            throw new Swift_IoException('Unable to open file for copying ['.$this->_path.']');
222        }
223        fseek($tmpFile, 0, SEEK_SET);
224        while (!feof($source)) {
225            fwrite($tmpFile, fread($source, 4096));
226        }
227        fseek($tmpFile, $currentPos, SEEK_SET);
228        fclose($source);
229        $this->_reader = $tmpFile;
230    }
231}
232