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

344 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace UserAccessManager\File;
6
7 use JetBrains\PhpStorm\NoReturn;
8 use UserAccessManager\Config\MainConfig;
9 use UserAccessManager\Config\WordpressConfig;
10 use UserAccessManager\Wrapper\Php;
11 use UserAccessManager\Wrapper\Wordpress;
12
13 class FileHandler
14 {
15 public const X_SEND_FILE_TEST_FILE = 'xSendFileTestFile';
16
17 public function __construct(
18 private Php $php,
19 private Wordpress $wordpress,
20 private WordpressConfig $wordpressConfig,
21 private MainConfig $mainConfig,
22 private FileProtectionFactory $fileProtectionFactory
23 ) {
24 }
25
26 private function clearBuffer(): void
27 {
28 //prevent '\n' / '0A'
29 if ((int) $this->php->iniGet('output_buffering') === 0
30 && is_numeric(ob_get_length()) === true
31 ) {
32 ob_clean();
33 }
34
35 flush();
36 }
37
38 private function getFileMineType(string $file): string
39 {
40 $fileName = basename($file);
41
42 /*
43 * This only for compatibility
44 * mime_content_type has been deprecated as the PECL extension file info
45 * provides the same functionality (and more) in a much cleaner way.
46 */
47 $explodedFileName = explode('.', $fileName);
48 $lastElement = array_pop($explodedFileName);
49 $fileExt = strtolower($lastElement);
50
51 $mimeTypes = $this->wordpressConfig->getMimeTypes();
52
53 if ($this->php->functionExists('finfo_open') === true) {
54 $fileInfo = finfo_open(FILEINFO_MIME);
55 $fileMimeType = finfo_file($fileInfo, $file);
56 finfo_close($fileInfo);
57 } elseif ($this->php->functionExists('mime_content_type')) {
58 $fileMimeType = mime_content_type($file);
59 } elseif (isset($mimeTypes[$fileExt]) === true) {
60 $fileMimeType = $mimeTypes[$fileExt];
61 } else {
62 $fileMimeType = 'application/octet-stream';
63 }
64
65 return (string) $fileMimeType;
66 }
67
68 private function addDefaultHeader(string $file, bool $isInline): void
69 {
70 $fileMimeType = $this->getFileMineType($file);
71 $contentDisposition = ($isInline === true) ? 'inline' : 'attachment';
72 $baseName = str_replace(' ', '_', basename($file));
73
74 header('Content-Description: File Transfer');
75 header('Content-Type: ' . $fileMimeType);
76 header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
77 }
78
79 private function deliverFileViaFopen(string $file): void
80 {
81 $handler = fopen($file, 'r');
82
83 while (feof($handler) === false) {
84 if ($this->php->iniGet('safe_mode') !== '') {
85 $this->php->setTimeLimit(30);
86 }
87
88 echo $this->php->fread($handler, 1024);
89 }
90 }
91
92 private function deliverFile(string $file, bool $isInline): void
93 {
94 header("HTTP/1.1 200 OK");
95 $downloadType = $this->mainConfig->getDownloadType();
96
97 if ($downloadType === 'xsendfile') {
98 header("X-Sendfile: $file");
99 }
100
101 $this->addDefaultHeader($file, $isInline);
102
103 if ($downloadType !== 'xsendfile') {
104 header('Content-Transfer-Encoding: binary');
105 header('Content-Length: ' . filesize($file));
106 $this->clearBuffer();
107
108 if ($downloadType === 'fopen') {
109 $this->deliverFileViaFopen($file);
110 } else {
111 readfile($file);
112 }
113 }
114 }
115
116 private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
117 {
118 //Figure out download piece from range (if set)
119 $seek = explode('-', $range);
120 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
121 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
122 $maxSize = $fileSize - 1;
123
124 if ($seekStart === null) {
125 $seekStart = $fileSize - $seekEnd;
126 $seekEnd = $maxSize;
127 } elseif ($seekEnd === null) {
128 $seekEnd = $maxSize;
129 }
130
131 //Start and end based on range (if set), else set defaults also check for invalid ranges.
132 $seekEnd = min($seekEnd, $maxSize);
133
134 return $seekStart < $seekEnd;
135 }
136
137 private function readFilePartly($fileHandler, int $bytes): void
138 {
139 $bytesLeft = $bytes;
140 $bufferSize = 1024;
141
142 while ($bytesLeft > 0 && feof($fileHandler) === false) {
143 $bytesToRead = min($bytesLeft, $bufferSize);
144 $bytesLeft -= $bytesToRead;
145 echo $this->php->fread($fileHandler, $bytesToRead);
146 $this->clearBuffer();
147
148 if ($this->php->connectionStatus() !== 0) {
149 $this->php->fClose($fileHandler);
150 break;
151 }
152 }
153 }
154
155 private function getRanges(int $fileSize): array
156 {
157 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
158 $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
159 $originRanges = explode(',', $originRanges);
160 $sizeUnit = $httpRange[0];
161 $ranges = [];
162
163 if ($sizeUnit === 'bytes') {
164 foreach ($originRanges as $originRange) {
165 if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
166 $ranges = [];
167 break;
168 }
169
170 $ranges[] = [$seekStart, $seekEnd];
171 }
172 }
173
174 return $ranges;
175 }
176
177 private function getExtraContents(string $file, array $ranges, ?int &$contentLength, ?string &$boundary): array
178 {
179 $contentLength = 0;
180 $extraContents = [];
181
182 //More than one range is requested?
183 if (count($ranges) > 1) {
184 $boundary = 'g45d64df96bmdf4sdgh45hf5';
185 $fullBoundary = "\r\n--$boundary--\r\n";
186 $fileSize = filesize($file);
187 $mineType = $this->getFileMineType($file);
188
189 //compute content length
190 foreach ($ranges as $index => $range) {
191 [$seekStart, $seekEnd] = $range;
192 $extraContent = $fullBoundary;
193 $extraContent .= "Content-Type: $mineType\r\n";
194 $extraContent .= "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
195 $extraContents[$index] = $extraContent;
196 $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
197 }
198
199 $contentLength += strlen($fullBoundary);
200 $extraContents[] = $fullBoundary;
201 }
202
203 return $extraContents;
204 }
205
206 private function deliverFilePartial(string $file, bool $isInline): void
207 {
208 $fileSize = filesize($file);
209 $ranges = $this->getRanges($fileSize);
210
211 if ($ranges !== []) {
212 $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
213
214 header('HTTP/1.1 206 Partial Content');
215 header('Content-Transfer-Encoding: binary');
216 header('Accept-Ranges: bytes');
217
218 if ($extraContents === []) {
219 $this->addDefaultHeader($file, $isInline);
220 [$seekStart, $seekEnd] = $ranges[0];
221 $contentLength = ($seekEnd - $seekStart + 1);
222 header("Content-Range: bytes $seekStart-$seekEnd/$fileSize");
223 } else {
224 header("Content-Type: multipart/x-byteranges; boundary=$boundary");
225 }
226
227 header("Content-Length: $contentLength");
228 $fileHandler = fopen($file, 'r');
229
230 foreach ($ranges as $index => $range) {
231 if (isset($extraContents[$index]) === true) {
232 echo $extraContents[$index];
233 }
234
235 [$seekStart, $seekEnd] = $ranges[0];
236 fseek($fileHandler, $seekStart);
237 $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
238 }
239
240 if ($extraContents !== []) {
241 echo end($extraContents);
242 $this->clearBuffer();
243 }
244 } else {
245 header('HTTP/1.1 416 Requested Range Not Satisfiable');
246 header("Content-Range: */$fileSize");
247 }
248 }
249
250 private function isInlineFile(string $file): bool
251 {
252 $inlineFiles = array_map('trim', explode(',', (string) $this->mainConfig->getInlineFiles()));
253 $map = array_flip($inlineFiles);
254 $extension = pathinfo($file, PATHINFO_EXTENSION);
255
256 return isset($map[$extension]);
257 }
258
259 #[NoReturn]
260 public function getFile(string $file, bool $isImage): void
261 {
262 //Deliver content
263 if (file_exists($file) === true) {
264 $isInline = $isImage === true || $this->isInlineFile($file) === true;
265
266 if (isset($_SERVER['HTTP_RANGE']) === true
267 && isset($_SERVER['REQUEST_METHOD']) === true
268 && $_SERVER['REQUEST_METHOD'] === 'GET'
269 ) {
270 $this->deliverFilePartial($file, $isInline);
271 } else {
272 $this->deliverFile($file, $isInline);
273 }
274
275 $this->php->callExit();
276 } else {
277 $this->wordpress->wpDie(
278 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
279 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
280 ['response' => 404]
281 );
282 }
283 }
284
285 private function getCurrentFileProtectionHandler(): FileProtectionInterface
286 {
287 if ($this->wordpress->isNginx() === true) {
288 return $this->fileProtectionFactory->createNginxFileProtection();
289 }
290
291 return $this->fileProtectionFactory->createApacheFileProtection();
292 }
293
294 public function getFileProtectionFileName(): string
295 {
296 return $this->getCurrentFileProtectionHandler()->getFileNameWithPath(
297 $this->wordpressConfig->getUploadDirectory()
298 );
299 }
300
301 public function createFileProtection(string $dir = null, string $objectType = null): bool
302 {
303 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
304
305 if ($dir !== null) {
306 return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
307 }
308
309 return false;
310 }
311
312 public function deleteFileProtection(string $dir = null): bool
313 {
314 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
315
316 if ($dir !== null) {
317 return $this->getCurrentFileProtectionHandler()->delete($dir);
318 }
319
320 return false;
321 }
322
323 #[NoReturn]
324 public function deliverXSendFileTestFile(): void
325 {
326 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
327 file_put_contents($file, 'success');
328
329 header("X-Sendfile: $file");
330 header('Content-Type: application/octet-stream');
331 header('Content-Disposition: attachment; filename="' . basename($file) . '"');
332 $this->php->callExit();
333 }
334
335 public function removeXSendFileTestFile(): void
336 {
337 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
338
339 if (file_exists($file) === true) {
340 unlink($file);
341 }
342 }
343 }
344