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

485 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 header("HTTP/1.1 200 OK");
172 $downloadType = $this->mainConfig->getDownloadType();
173
174 if ($downloadType === 'xsendfile') {
175 header("X-Sendfile: {$file}");
176 }
177
178 $this->addDefaultHeader($file, $isInline);
179
180 if ($downloadType !== 'xsendfile') {
181 header('Content-Transfer-Encoding: binary');
182 header('Content-Length: ' . filesize($file));
183 $this->clearBuffer();
184
185 if ($downloadType === 'fopen') {
186 $this->deliverFileViaFopen($file);
187 } else {
188 readfile($file);
189 }
190 }
191 }
192
193 /**
194 * Sets the seek start and end.
195 * @param string $range
196 * @param int $fileSize
197 * @param int|null $seekStart
198 * @param int|null $seekEnd
199 * @return bool
200 */
201 private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
202 {
203 //Figure out download piece from range (if set)
204 $seek = explode('-', $range);
205 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
206 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
207 $maxSize = $fileSize - 1;
208
209 if ($seekStart === null) {
210 $seekStart = $fileSize - $seekEnd;
211 $seekEnd = $maxSize;
212 } elseif ($seekEnd === null) {
213 $seekEnd = $maxSize;
214 }
215
216 //Start and end based on range (if set), else set defaults also check for invalid ranges.
217 $seekEnd = min($seekEnd, $maxSize);
218
219 return $seekStart < $seekEnd;
220 }
221
222 /**
223 * Reads the file partly.
224 * @param resource $fileHandler
225 * @param int $bytes
226 */
227 private function readFilePartly($fileHandler, int $bytes)
228 {
229 $bytesLeft = $bytes;
230 $bufferSize = 1024;
231
232 while ($bytesLeft > 0 && feof($fileHandler) === false) {
233 $bytesToRead = min($bytesLeft, $bufferSize);
234 $bytesLeft -= $bytesToRead;
235 echo $this->php->fread($fileHandler, $bytesToRead);
236 $this->clearBuffer();
237
238 if ($this->php->connectionStatus() !== 0) {
239 $this->php->fClose($fileHandler);
240 break;
241 }
242 }
243 }
244
245 /**
246 * Returns the http ranges.
247 * @param int $fileSize
248 * @return array
249 */
250 private function getRanges(int $fileSize): array
251 {
252 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
253 $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
254 $originRanges = explode(',', $originRanges);
255 $sizeUnit = $httpRange[0];
256 $ranges = [];
257
258 if ($sizeUnit === 'bytes') {
259 foreach ($originRanges as $originRange) {
260 if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
261 $ranges = [];
262 break;
263 }
264
265 $ranges[] = [$seekStart, $seekEnd];
266 }
267 }
268
269 return $ranges;
270 }
271
272 /**
273 * Returns the extra contents.
274 * @param string $file
275 * @param array $ranges
276 * @param int|null $contentLength
277 * @param string|null $boundary
278 * @return array
279 */
280 private function getExtraContents(string $file, array $ranges, ?int &$contentLength, ?string &$boundary): array
281 {
282 $contentLength = 0;
283 $extraContents = [];
284
285 //More than one range is requested?
286 if (count($ranges) > 1) {
287 $boundary = 'g45d64df96bmdf4sdgh45hf5';
288 $fullBoundary = "\r\n--{$boundary}--\r\n";
289 $fileSize = filesize($file);
290 $mineType = $this->getFileMineType($file);
291
292 //compute content length
293 foreach ($ranges as $index => $range) {
294 list($seekStart, $seekEnd) = $range;
295 $extraContent = $fullBoundary;
296 $extraContent .= "Content-Type: {$mineType}\r\n";
297 $extraContent .= "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
298 $extraContents[$index] = $extraContent;
299 $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
300 }
301
302 $contentLength += strlen($fullBoundary);
303 $extraContents[] = $fullBoundary;
304 }
305
306 return $extraContents;
307 }
308
309 /**
310 * Delivers the file partial.
311 * @param string $file
312 * @param bool $isInline
313 */
314 private function deliverFilePartial(string $file, bool $isInline)
315 {
316 $fileSize = filesize($file);
317 $ranges = $this->getRanges($fileSize);
318
319 if ($ranges !== []) {
320 $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
321
322 header('HTTP/1.1 206 Partial Content');
323 header('Content-Transfer-Encoding: binary');
324 header('Accept-Ranges: bytes');
325
326 if ($extraContents === []) {
327 $this->addDefaultHeader($file, $isInline);
328 list($seekStart, $seekEnd) = $ranges[0];
329 $contentLength = ($seekEnd - $seekStart + 1);
330 header("Content-Range: bytes {$seekStart}-{$seekEnd}/{$fileSize}");
331 } else {
332 header("Content-Type: multipart/x-byteranges; boundary={$boundary}");
333 }
334
335 header("Content-Length: {$contentLength}");
336 $fileHandler = fopen($file, 'r');
337
338 foreach ($ranges as $index => $range) {
339 if (isset($extraContents[$index]) === true) {
340 echo $extraContents[$index];
341 }
342
343 list($seekStart, $seekEnd) = $ranges[0];
344 fseek($fileHandler, $seekStart);
345 $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
346 }
347
348 if ($extraContents !== []) {
349 echo end($extraContents);
350 $this->clearBuffer();
351 }
352 } else {
353 header('HTTP/1.1 416 Requested Range Not Satisfiable');
354 header("Content-Range: */$fileSize");
355 }
356 }
357
358 /**
359 * Checks if the file is an inline file
360 * @param string $file
361 * @return bool
362 */
363 private function isInlineFile(string $file): bool
364 {
365 $inlineFiles = array_map('trim', explode(',', (string) $this->mainConfig->getInlineFiles()));
366 $map = array_flip($inlineFiles);
367 $extension = pathinfo($file, PATHINFO_EXTENSION);
368
369 return isset($map[$extension]);
370 }
371
372 /**
373 * Delivers the content of the requested file.
374 * @param string $file
375 * @param bool $isImage
376 */
377 public function getFile(string $file, bool $isImage)
378 {
379 //Deliver content
380 if (file_exists($file) === true) {
381 $isInline = $isImage === true || $this->isInlineFile($file) === true;
382
383 if (isset($_SERVER['HTTP_RANGE']) === true
384 && isset($_SERVER['REQUEST_METHOD']) === true
385 && $_SERVER['REQUEST_METHOD'] === 'GET'
386 ) {
387 $this->deliverFilePartial($file, $isInline);
388 } else {
389 $this->deliverFile($file, $isInline);
390 }
391
392 $this->php->callExit();
393 } else {
394 $this->wordpress->wpDie(
395 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
396 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
397 ['response' => 404]
398 );
399 }
400 }
401
402 /**
403 * Returns the current file protection handler.
404 * @return FileProtectionInterface
405 */
406 private function getCurrentFileProtectionHandler(): FileProtectionInterface
407 {
408 if ($this->wordpress->isNginx() === true) {
409 return $this->fileProtectionFactory->createNginxFileProtection();
410 }
411
412 return $this->fileProtectionFactory->createApacheFileProtection();
413 }
414
415 /**
416 * Returns the file protection file.
417 * @return string
418 */
419 public function getFileProtectionFileName(): string
420 {
421 return $this->getCurrentFileProtectionHandler()->getFileNameWithPath(
422 $this->wordpressConfig->getUploadDirectory()
423 );
424 }
425
426 /**
427 * Creates a protection file.
428 * @param string $dir The destination directory.
429 * @param string $objectType The object type.
430 * @return false
431 */
432 public function createFileProtection($dir = null, $objectType = null): bool
433 {
434 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
435
436 if ($dir !== null) {
437 return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
438 }
439
440 return false;
441 }
442
443 /**
444 * Deletes the protection files.
445 * @param string $dir The destination directory.
446 * @return false
447 */
448 public function deleteFileProtection($dir = null): bool
449 {
450 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
451
452 if ($dir !== null) {
453 return $this->getCurrentFileProtectionHandler()->delete($dir);
454 }
455
456 return false;
457 }
458
459 /**
460 * Delivers a xsendfile test file.
461 */
462 public function deliverXSendFileTestFile()
463 {
464 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
465 file_put_contents($file, 'success');
466
467 header("X-Sendfile: {$file}");
468 header('Content-Type: application/octet-stream');
469 header('Content-Disposition: attachment; filename="' . basename($file) . '"');
470 $this->php->callExit();
471 }
472
473 /**
474 * Removes the xsendfile test file if exists.
475 */
476 public function removeXSendFileTestFile()
477 {
478 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
479
480 if (file_exists($file) === true) {
481 unlink($file);
482 }
483 }
484 }
485