PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / includes / class-partialfileservlet.php

class-partialfileservlet.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at includes/class-partialfileservlet.php

267 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // https://stackoverflow.com/questions/157318/resumable-downloads-when-using-php-to-send-the-file
4 // User: DaveRandom
5
6 if (!class_exists('UpdraftPlus_NonExistentFileException')):
7 class UpdraftPlus_NonExistentFileException extends RuntimeException {}
8 endif;
9
10 if (!class_exists('UpdraftPlus_UnreadableFileException')):
11 class UpdraftPlus_UnreadableFileException extends RuntimeException {}
12 endif;
13
14 if (!class_exists('UpdraftPlus_UnsatisfiableRangeException')):
15 class UpdraftPlus_UnsatisfiableRangeException extends RuntimeException {}
16 endif;
17
18 if (!class_exists('UpdraftPlus_InvalidRangeHeaderException')):
19 class UpdraftPlus_InvalidRangeHeaderException extends RuntimeException {}
20 endif;
21
22 class UpdraftPlus_RangeHeader
23 {
24 /**
25 * The first byte in the file to send (0-indexed), a null value indicates the last
26 * $end bytes
27 *
28 * @var int|null
29 */
30 private $firstByte;
31
32 /**
33 * The last byte in the file to send (0-indexed), a null value indicates $start to
34 * EOF
35 *
36 * @var int|null
37 */
38 private $lastByte;
39
40 /**
41 * Create a new instance from a Range header string
42 *
43 * @param string $header
44 * @return UpdraftPlus_RangeHeader
45 */
46 public static function createFromHeaderString($header)
47 {
48 if ($header === null) {
49 return null;
50 }
51 if (!preg_match('/^\s*([A-Za-z]+)\s*=\s*(\d*)\s*-\s*(\d*)\s*(?:,|$)/', $header, $info)) {
52 throw new UpdraftPlus_InvalidRangeHeaderException('Invalid header format');
53 } else if (strtolower($info[1]) !== 'bytes') {
54 throw new UpdraftPlus_InvalidRangeHeaderException('Unknown range unit: ' . $info[1]);
55 }
56
57 return new self(
58 $info[2] === '' ? null : $info[2],
59 $info[3] === '' ? null : $info[3]
60 );
61 }
62
63 /**
64 * @param int|null $firstByte
65 * @param int|null $lastByte
66 * @throws UpdraftPlus_InvalidRangeHeaderException
67 */
68 public function __construct($firstByte, $lastByte)
69 {
70 $this->firstByte = $firstByte === null ? $firstByte : (int)$firstByte;
71 $this->lastByte = $lastByte === null ? $lastByte : (int)$lastByte;
72
73 if ($this->firstByte === null && $this->lastByte === null) {
74 throw new UpdraftPlus_InvalidRangeHeaderException(
75 'Both start and end position specifiers empty'
76 );
77 } else if ($this->firstByte < 0 || $this->lastByte < 0) {
78 throw new UpdraftPlus_InvalidRangeHeaderException(
79 'Position specifiers cannot be negative'
80 );
81 } else if ($this->lastByte !== null && $this->lastByte < $this->firstByte) {
82 throw new UpdraftPlus_InvalidRangeHeaderException(
83 'Last byte cannot be less than first byte'
84 );
85 }
86 }
87
88 /**
89 * Get the start position when this range is applied to a file of the specified size
90 *
91 * @param int $fileSize
92 * @return int
93 * @throws UpdraftPlus_UnsatisfiableRangeException
94 */
95 public function getStartPosition($fileSize)
96 {
97 $size = (int)$fileSize;
98
99 if ($this->firstByte === null) {
100 return ($size - 1) - $this->lastByte;
101 }
102
103 if ($size <= $this->firstByte) {
104 throw new UpdraftPlus_UnsatisfiableRangeException(
105 'Start position is after the end of the file'
106 );
107 }
108
109 return $this->firstByte;
110 }
111
112 /**
113 * Get the end position when this range is applied to a file of the specified size
114 *
115 * @param int $fileSize
116 * @return int
117 * @throws UpdraftPlus_UnsatisfiableRangeException
118 */
119 public function getEndPosition($fileSize)
120 {
121 $size = (int)$fileSize;
122
123 if ($this->lastByte === null) {
124 return $size - 1;
125 }
126
127 if ($size <= $this->lastByte) {
128 throw new UpdraftPlus_UnsatisfiableRangeException(
129 'End position is after the end of the file'
130 );
131 }
132
133 return $this->lastByte;
134 }
135
136 /**
137 * Get the length when this range is applied to a file of the specified size
138 *
139 * @param int $fileSize
140 * @return int
141 * @throws UpdraftPlus_UnsatisfiableRangeException
142 */
143 public function getLength($fileSize)
144 {
145 $size = (int)$fileSize;
146
147 return $this->getEndPosition($size) - $this->getStartPosition($size) + 1;
148 }
149
150 /**
151 * Get a Content-Range header corresponding to this Range and the specified file
152 * size
153 *
154 * @param int $fileSize
155 * @return string
156 */
157 public function getContentRangeHeader($fileSize)
158 {
159 return 'bytes ' . $this->getStartPosition($fileSize) . '-'
160 . $this->getEndPosition($fileSize) . '/' . $fileSize;
161 }
162 }
163
164 class UpdraftPlus_PartialFileServlet
165 {
166 /**
167 * The range header on which the data transmission will be based
168 *
169 * @var UpdraftPlus_RangeHeader|null
170 */
171 private $range;
172
173 /**
174 * @param UpdraftPlus_RangeHeader $range Range header on which the transmission will be based
175 */
176 public function __construct($range = null)
177 {
178 $this->range = $range;
179 }
180
181 /**
182 * Send part of the data in a seekable stream resource to the output buffer
183 *
184 * @param resource $fp Stream resource to read data from
185 * @param int $start Position in the stream to start reading
186 * @param int $length Number of bytes to read
187 * @param int $chunkSize Maximum bytes to read from the file in a single operation
188 */
189 private function sendDataRange($fp, $start, $length, $chunkSize = 2097152)
190 {
191 if ($start > 0) {
192 fseek($fp, $start, SEEK_SET);
193 }
194
195 while ($length) {
196 $read = ($length > $chunkSize) ? $chunkSize : $length;
197 $length -= $read;
198 echo fread($fp, $read);
199 }
200 }
201
202 /**
203 * Send the headers that are included regardless of whether a range was requested
204 *
205 * @param string $fileName
206 * @param int $contentLength
207 * @param string $contentType
208 */
209 private function sendDownloadHeaders($fileName, $contentLength, $contentType)
210 {
211 header('Content-Type: ' . $contentType);
212 header('Content-Length: ' . $contentLength);
213 header('Content-Disposition: attachment; filename="' . $fileName . '"');
214 header('Accept-Ranges: bytes');
215 }
216
217 /**
218 * Send data from a file based on the current Range header
219 *
220 * @param string $path Local file system path to serve
221 * @param string $contentType MIME type of the data stream
222 */
223 public function sendFile($path, $contentType = 'application/octet-stream')
224 {
225 // Make sure the file exists and is a file, otherwise we are wasting our time
226 $localPath = realpath($path);
227 if ($localPath === false || !is_file($localPath)) {
228 throw new UpdraftPlus_NonExistentFileException(
229 $path . ' does not exist or is not a file'
230 );
231 }
232
233 // Make sure we can open the file for reading
234 if (!$fp = fopen($localPath, 'r')) {
235 throw new UpdraftPlus_UnreadableFileException(
236 'Failed to open ' . $localPath . ' for reading'
237 );
238 }
239
240 $fileSize = filesize($localPath);
241
242 if ($this->range == null) {
243 // No range requested, just send the whole file
244 header('HTTP/1.1 200 OK');
245 $this->sendDownloadHeaders(basename($localPath), $fileSize, $contentType);
246
247 fpassthru($fp);
248 } else {
249 // Send the request range
250 header('HTTP/1.1 206 Partial Content');
251 header('Content-Range: ' . $this->range->getContentRangeHeader($fileSize));
252 $this->sendDownloadHeaders(
253 basename($localPath),
254 $this->range->getLength($fileSize),
255 $contentType
256 );
257
258 $this->sendDataRange(
259 $fp,
260 $this->range->getStartPosition($fileSize),
261 $this->range->getLength($fileSize)
262 );
263 }
264
265 fclose($fp);
266 }
267 }