PluginProbe
User Access Manager / 2.1.10
User Access Manager v2.1.10
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.1.10, at src/File/FileHandler.php

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