PluginProbe
User Access Manager / 2.2.16
User Access Manager v2.2.16
2.3.20 2.3.19 2.3.18 2.3.17 2.3.16 2.3.15 2.3.14 2.3.13 trunk 0.6 0.6.1 0.6.2 0.7 0.7 Beta 0.7.0.1 0.8 0.8.0.1 0.8.0.2 0.9 0.9.1 0.9.1.1 0.9.1.2 0.9.1.3 0.9.1.4 1.0 All 136 releases
user-access-manager / src / File / FileHandler.php

FileHandler.php in User Access Manager 2.2.16, at src/File/FileHandler.php

484 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * FileHandler.php
4 *
5 * The FileHandler class file.
6 *
7 * PHP versions 5
8 *
9 * @author Alexander Schneider <alexanderschneider85@gmail.com>
10 * @copyright 2008-2017 Alexander Schneider
11 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU General Public License, version 2
12 * @version SVN: $id$
13 * @link http://wordpress.org/extend/plugins/user-access-manager/
14 */
15
16 declare(strict_types=1);
17
18 namespace UserAccessManager\File;
19
20 use UserAccessManager\Config\MainConfig;
21 use UserAccessManager\Config\WordpressConfig;
22 use UserAccessManager\Wrapper\Php;
23 use UserAccessManager\Wrapper\Wordpress;
24
25 /**
26 * Class FileHandler
27 *
28 * @package UserAccessManager\FileHandler
29 */
30 class FileHandler
31 {
32 const X_SEND_FILE_TEST_FILE = 'xSendFileTestFile';
33
34 /**
35 * @var Php
36 */
37 private $php;
38
39 /**
40 * @var Wordpress
41 */
42 private $wordpress;
43
44 /**
45 * @var WordpressConfig
46 */
47 private $wordpressConfig;
48
49 /**
50 * @var MainConfig
51 */
52 private $mainConfig;
53
54 /**
55 * @var FileProtectionFactory
56 */
57 private $fileProtectionFactory;
58
59 /**
60 * FileHandler constructor.
61 * @param Php $php
62 * @param Wordpress $wordpress
63 * @param WordpressConfig $wordpressConfig
64 * @param MainConfig $mainConfig
65 * @param FileProtectionFactory $fileProtectionFactory
66 */
67 public function __construct(
68 Php $php,
69 Wordpress $wordpress,
70 WordpressConfig $wordpressConfig,
71 MainConfig $mainConfig,
72 FileProtectionFactory $fileProtectionFactory
73 ) {
74 $this->php = $php;
75 $this->wordpress = $wordpress;
76 $this->wordpressConfig = $wordpressConfig;
77 $this->mainConfig = $mainConfig;
78 $this->fileProtectionFactory = $fileProtectionFactory;
79 }
80
81 /**
82 * Clears the buffer.
83 */
84 private function clearBuffer()
85 {
86 //prevent '\n' / '0A'
87 if ((int) $this->php->iniGet('output_buffering') === 0
88 && is_numeric(ob_get_length()) === true
89 ) {
90 ob_clean();
91 }
92
93 flush();
94 }
95
96 /**
97 * Returns the file mine type.
98 * @param string $file
99 * @return string
100 */
101 private function getFileMineType(string $file): string
102 {
103 $fileName = basename($file);
104
105 /*
106 * This only for compatibility
107 * mime_content_type has been deprecated as the PECL extension file info
108 * provides the same functionality (and more) in a much cleaner way.
109 */
110 $explodedFileName = explode('.', $fileName);
111 $lastElement = array_pop($explodedFileName);
112 $fileExt = strtolower($lastElement);
113
114 $mimeTypes = $this->wordpressConfig->getMimeTypes();
115
116 if ($this->php->functionExists('finfo_open') === true) {
117 $fileInfo = finfo_open(FILEINFO_MIME);
118 $fileMimeType = finfo_file($fileInfo, $file);
119 finfo_close($fileInfo);
120 } elseif ($this->php->functionExists('mime_content_type')) {
121 $fileMimeType = mime_content_type($file);
122 } elseif (isset($mimeTypes[$fileExt]) === true) {
123 $fileMimeType = $mimeTypes[$fileExt];
124 } else {
125 $fileMimeType = 'application/octet-stream';
126 }
127
128 return (string) $fileMimeType;
129 }
130
131 /**
132 * Adds the default header.
133 * @param string $file
134 * @param bool $isInline
135 */
136 private function addDefaultHeader(string $file, bool $isInline)
137 {
138 $fileMimeType = $this->getFileMineType($file);
139 $contentDisposition = ($isInline === true) ? 'inline' : 'attachment';
140 $baseName = str_replace(' ', '_', basename($file));
141
142 header('Content-Description: File Transfer');
143 header('Content-Type: ' . $fileMimeType);
144 header("Content-Disposition: {$contentDisposition}; filename=\"{$baseName}\"");
145 }
146
147 /**
148 * Delivers the file via fopen.
149 * @param string $file
150 */
151 private function deliverFileViaFopen(string $file)
152 {
153 $handler = fopen($file, 'r');
154
155 while (feof($handler) === false) {
156 if ($this->php->iniGet('safe_mode') !== '') {
157 $this->php->setTimeLimit(30);
158 }
159
160 echo $this->php->fread($handler, 1024);
161 }
162 }
163
164 /**
165 * Delivers the file.
166 * @param string $file
167 * @param bool $isInline
168 */
169 private function deliverFile(string $file, bool $isInline)
170 {
171 $downloadType = $this->mainConfig->getDownloadType();
172
173 if ($downloadType === 'xsendfile') {
174 header("X-Sendfile: {$file}");
175 }
176
177 $this->addDefaultHeader($file, $isInline);
178
179 if ($downloadType !== 'xsendfile') {
180 header('Content-Transfer-Encoding: binary');
181 header('Content-Length: ' . filesize($file));
182 $this->clearBuffer();
183
184 if ($downloadType === 'fopen') {
185 $this->deliverFileViaFopen($file);
186 } else {
187 readfile($file);
188 }
189 }
190 }
191
192 /**
193 * Sets the seek start and end.
194 * @param string $range
195 * @param int $fileSize
196 * @param int|null $seekStart
197 * @param int|null $seekEnd
198 * @return bool
199 */
200 private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
201 {
202 //Figure out download piece from range (if set)
203 $seek = explode('-', $range);
204 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
205 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
206 $maxSize = $fileSize - 1;
207
208 if ($seekStart === null) {
209 $seekStart = $fileSize - $seekEnd;
210 $seekEnd = $maxSize;
211 } elseif ($seekEnd === null) {
212 $seekEnd = $maxSize;
213 }
214
215 //Start and end based on range (if set), else set defaults also check for invalid ranges.
216 $seekEnd = min($seekEnd, $maxSize);
217
218 return $seekStart < $seekEnd;
219 }
220
221 /**
222 * Reads the file partly.
223 * @param resource $fileHandler
224 * @param int $bytes
225 */
226 private function readFilePartly($fileHandler, int $bytes)
227 {
228 $bytesLeft = $bytes;
229 $bufferSize = 1024;
230
231 while ($bytesLeft > 0 && feof($fileHandler) === false) {
232 $bytesToRead = min($bytesLeft, $bufferSize);
233 $bytesLeft -= $bytesToRead;
234 echo $this->php->fread($fileHandler, $bytesToRead);
235 $this->clearBuffer();
236
237 if ($this->php->connectionStatus() !== 0) {
238 $this->php->fClose($fileHandler);
239 break;
240 }
241 }
242 }
243
244 /**
245 * Returns the http ranges.
246 * @param int $fileSize
247 * @return array
248 */
249 private function getRanges(int $fileSize): array
250 {
251 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
252 $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
253 $originRanges = explode(',', $originRanges);
254 $sizeUnit = $httpRange[0];
255 $ranges = [];
256
257 if ($sizeUnit === 'bytes') {
258 foreach ($originRanges as $originRange) {
259 if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
260 $ranges = [];
261 break;
262 }
263
264 $ranges[] = [$seekStart, $seekEnd];
265 }
266 }
267
268 return $ranges;
269 }
270
271 /**
272 * Returns the extra contents.
273 * @param string $file
274 * @param array $ranges
275 * @param int|null $contentLength
276 * @param string|null $boundary
277 * @return array
278 */
279 private function getExtraContents(string $file, array $ranges, ?int &$contentLength, ?string &$boundary): array
280 {
281 $contentLength = 0;
282 $extraContents = [];
283
284 //More than one range is requested?
285 if (count($ranges) > 1) {
286 $boundary = 'g45d64df96bmdf4sdgh45hf5';
287 $fullBoundary = "\r\n--{$boundary}--\r\n";
288 $fileSize = filesize($file);
289 $mineType = $this->getFileMineType($file);
290
291 //compute content length
292 foreach ($ranges as $index => $range) {
293 list($seekStart, $seekEnd) = $range;
294 $extraContent = $fullBoundary;
295 $extraContent .= "Content-Type: {$mineType}\r\n";
296 $extraContent .= "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
297 $extraContents[$index] = $extraContent;
298 $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
299 }
300
301 $contentLength += strlen($fullBoundary);
302 $extraContents[] = $fullBoundary;
303 }
304
305 return $extraContents;
306 }
307
308 /**
309 * Delivers the file partial.
310 * @param string $file
311 * @param bool $isInline
312 */
313 private function deliverFilePartial(string $file, bool $isInline)
314 {
315 $fileSize = filesize($file);
316 $ranges = $this->getRanges($fileSize);
317
318 if ($ranges !== []) {
319 $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
320
321 header('HTTP/1.1 206 Partial Content');
322 header('Content-Transfer-Encoding: binary');
323 header('Accept-Ranges: bytes');
324
325 if ($extraContents === []) {
326 $this->addDefaultHeader($file, $isInline);
327 list($seekStart, $seekEnd) = $ranges[0];
328 $contentLength = ($seekEnd - $seekStart + 1);
329 header("Content-Range: bytes {$seekStart}-{$seekEnd}/{$fileSize}");
330 } else {
331 header("Content-Type: multipart/x-byteranges; boundary={$boundary}");
332 }
333
334 header("Content-Length: {$contentLength}");
335 $fileHandler = fopen($file, 'r');
336
337 foreach ($ranges as $index => $range) {
338 if (isset($extraContents[$index]) === true) {
339 echo $extraContents[$index];
340 }
341
342 list($seekStart, $seekEnd) = $ranges[0];
343 fseek($fileHandler, $seekStart);
344 $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
345 }
346
347 if ($extraContents !== []) {
348 echo end($extraContents);
349 $this->clearBuffer();
350 }
351 } else {
352 header('HTTP/1.1 416 Requested Range Not Satisfiable');
353 header("Content-Range: */$fileSize");
354 }
355 }
356
357 /**
358 * Checks if the file is an inline file
359 * @param string $file
360 * @return bool
361 */
362 private function isInlineFile(string $file): bool
363 {
364 $inlineFiles = array_map('trim', explode(',', (string) $this->mainConfig->getInlineFiles()));
365 $map = array_flip($inlineFiles);
366 $extension = pathinfo($file, PATHINFO_EXTENSION);
367
368 return isset($map[$extension]);
369 }
370
371 /**
372 * Delivers the content of the requested file.
373 * @param string $file
374 * @param bool $isImage
375 */
376 public function getFile(string $file, bool $isImage)
377 {
378 //Deliver content
379 if (file_exists($file) === true) {
380 $isInline = $isImage === true || $this->isInlineFile($file) === true;
381
382 if (isset($_SERVER['HTTP_RANGE']) === true
383 && isset($_SERVER['REQUEST_METHOD']) === true
384 && $_SERVER['REQUEST_METHOD'] === 'GET'
385 ) {
386 $this->deliverFilePartial($file, $isInline);
387 } else {
388 $this->deliverFile($file, $isInline);
389 }
390
391 $this->php->callExit();
392 } else {
393 $this->wordpress->wpDie(
394 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
395 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
396 ['response' => 404]
397 );
398 }
399 }
400
401 /**
402 * Returns the current file protection handler.
403 * @return FileProtectionInterface
404 */
405 private function getCurrentFileProtectionHandler(): FileProtectionInterface
406 {
407 if ($this->wordpress->isNginx() === true) {
408 return $this->fileProtectionFactory->createNginxFileProtection();
409 }
410
411 return $this->fileProtectionFactory->createApacheFileProtection();
412 }
413
414 /**
415 * Returns the file protection file.
416 * @return string
417 */
418 public function getFileProtectionFileName(): string
419 {
420 return $this->getCurrentFileProtectionHandler()->getFileNameWithPath(
421 $this->wordpressConfig->getUploadDirectory()
422 );
423 }
424
425 /**
426 * Creates a protection file.
427 * @param string $dir The destination directory.
428 * @param string $objectType The object type.
429 * @return false
430 */
431 public function createFileProtection($dir = null, $objectType = null): bool
432 {
433 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
434
435 if ($dir !== null) {
436 return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
437 }
438
439 return false;
440 }
441
442 /**
443 * Deletes the protection files.
444 * @param string $dir The destination directory.
445 * @return false
446 */
447 public function deleteFileProtection($dir = null): bool
448 {
449 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
450
451 if ($dir !== null) {
452 return $this->getCurrentFileProtectionHandler()->delete($dir);
453 }
454
455 return false;
456 }
457
458 /**
459 * Delivers a xsendfile test file.
460 */
461 public function deliverXSendFileTestFile()
462 {
463 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
464 file_put_contents($file, 'success');
465
466 header("X-Sendfile: {$file}");
467 header('Content-Type: application/octet-stream');
468 header('Content-Disposition: attachment; filename="' . basename($file) . '"');
469 $this->php->callExit();
470 }
471
472 /**
473 * Removes the xsendfile test file if exists.
474 */
475 public function removeXSendFileTestFile()
476 {
477 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
478
479 if (file_exists($file) === true) {
480 unlink($file);
481 }
482 }
483 }
484