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

355 lines 12.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 $this->php->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 $this->php->fInfoClose($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 $this->php->header('Content-Description: File Transfer');
75 $this->php->header('Content-Type: ' . $fileMimeType);
76 $this->php->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 $this->php->header("HTTP/1.1 200 OK");
95 $downloadType = $this->mainConfig->getDownloadType();
96
97 if ($downloadType === 'xsendfile') {
98 if ($this->wordpress->isNginx()) {
99 // Use /uam-files/ prefix so the internal redirect goes to a dedicated
100 // internal location that bypasses UAM's rewrite rules, avoiding a loop.
101 $uri = '/uam-files' . str_replace(rtrim(ABSPATH, '/'), '', $file);
102 $this->php->header("X-Accel-Redirect: $uri");
103 } elseif ($this->wordpress->isApacheModuleLoaded('mod_xsendfile')) {
104 $this->php->header("X-Sendfile: $file");
105 } else {
106 // mod_xsendfile is not available — fall back to fopen so the file
107 // is still delivered rather than sending an empty response.
108 $downloadType = 'fopen';
109 }
110 }
111
112 $this->addDefaultHeader($file, $isInline);
113
114 if ($downloadType !== 'xsendfile') {
115 $this->php->header('Content-Transfer-Encoding: binary');
116 $this->php->header('Content-Length: ' . filesize($file));
117 $this->clearBuffer();
118
119 if ($downloadType === 'fopen') {
120 $this->deliverFileViaFopen($file);
121 } else {
122 readfile($file);
123 }
124 }
125 }
126
127 private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
128 {
129 //Figure out download piece from range (if set)
130 $seek = explode('-', $range);
131 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
132 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
133 $maxSize = $fileSize - 1;
134
135 if ($seekStart === null) {
136 $seekStart = $fileSize - $seekEnd;
137 $seekEnd = $maxSize;
138 } elseif ($seekEnd === null) {
139 $seekEnd = $maxSize;
140 }
141
142 //Start and end based on range (if set), else set defaults also check for invalid ranges.
143 $seekEnd = min($seekEnd, $maxSize);
144
145 return $seekStart < $seekEnd;
146 }
147
148 private function readFilePartly($fileHandler, int $bytes): void
149 {
150 $bytesLeft = $bytes;
151 $bufferSize = 1024;
152
153 while ($bytesLeft > 0 && feof($fileHandler) === false) {
154 $bytesToRead = min($bytesLeft, $bufferSize);
155 $bytesLeft -= $bytesToRead;
156 echo $this->php->fread($fileHandler, $bytesToRead);
157 $this->clearBuffer();
158
159 if ($this->php->connectionStatus() !== 0) {
160 $this->php->fClose($fileHandler);
161 break;
162 }
163 }
164 }
165
166 private function getRanges(int $fileSize): array
167 {
168 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
169 $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
170 $originRanges = explode(',', $originRanges);
171 $sizeUnit = $httpRange[0];
172 $ranges = [];
173
174 if ($sizeUnit === 'bytes') {
175 foreach ($originRanges as $originRange) {
176 if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
177 $ranges = [];
178 break;
179 }
180
181 $ranges[] = [$seekStart, $seekEnd];
182 }
183 }
184
185 return $ranges;
186 }
187
188 private function getExtraContents(string $file, array $ranges, ?int &$contentLength, ?string &$boundary): array
189 {
190 $contentLength = 0;
191 $extraContents = [];
192
193 //More than one range is requested?
194 if (count($ranges) > 1) {
195 $boundary = 'g45d64df96bmdf4sdgh45hf5';
196 $fullBoundary = "\r\n--$boundary--\r\n";
197 $fileSize = filesize($file);
198 $mineType = $this->getFileMineType($file);
199
200 //compute content length
201 foreach ($ranges as $index => $range) {
202 [$seekStart, $seekEnd] = $range;
203 $extraContent = $fullBoundary;
204 $extraContent .= "Content-Type: $mineType\r\n";
205 $extraContent .= "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
206 $extraContents[$index] = $extraContent;
207 $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
208 }
209
210 $contentLength += strlen($fullBoundary);
211 $extraContents[] = $fullBoundary;
212 }
213
214 return $extraContents;
215 }
216
217 private function deliverFilePartial(string $file, bool $isInline): void
218 {
219 $fileSize = filesize($file);
220 $ranges = $this->getRanges($fileSize);
221
222 if ($ranges !== []) {
223 $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
224
225 $this->php->header('HTTP/1.1 206 Partial Content');
226 $this->php->header('Content-Transfer-Encoding: binary');
227 $this->php->header('Accept-Ranges: bytes');
228
229 if ($extraContents === []) {
230 $this->addDefaultHeader($file, $isInline);
231 [$seekStart, $seekEnd] = $ranges[0];
232 $contentLength = ($seekEnd - $seekStart + 1);
233 $this->php->header("Content-Range: bytes $seekStart-$seekEnd/$fileSize");
234 } else {
235 $this->php->header("Content-Type: multipart/x-byteranges; boundary=$boundary");
236 }
237
238 $this->php->header("Content-Length: $contentLength");
239 $fileHandler = fopen($file, 'r');
240
241 foreach ($ranges as $index => $range) {
242 if (isset($extraContents[$index]) === true) {
243 echo $extraContents[$index];
244 }
245
246 [$seekStart, $seekEnd] = $ranges[0];
247 $this->php->fseek($fileHandler, $seekStart);
248 $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
249 }
250
251 if ($extraContents !== []) {
252 echo end($extraContents);
253 $this->clearBuffer();
254 }
255 } else {
256 $this->php->header('HTTP/1.1 416 Requested Range Not Satisfiable');
257 $this->php->header("Content-Range: */$fileSize");
258 }
259 }
260
261 private function isInlineFile(string $file): bool
262 {
263 $inlineFiles = array_map('trim', explode(',', (string) $this->mainConfig->getInlineFiles()));
264 $map = array_flip($inlineFiles);
265 $extension = pathinfo($file, PATHINFO_EXTENSION);
266
267 return isset($map[$extension]);
268 }
269
270 #[NoReturn]
271 public function getFile(string $file, bool $isImage): void
272 {
273 //Deliver content
274 if (file_exists($file) === true) {
275 $isInline = $isImage === true || $this->isInlineFile($file) === true;
276
277 if (isset($_SERVER['HTTP_RANGE']) === true
278 && isset($_SERVER['REQUEST_METHOD']) === true
279 && $_SERVER['REQUEST_METHOD'] === 'GET'
280 ) {
281 $this->deliverFilePartial($file, $isInline);
282 } else {
283 $this->deliverFile($file, $isInline);
284 }
285
286 $this->php->callExit();
287 } else {
288 $this->wordpress->wpDie(
289 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
290 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
291 ['response' => 404]
292 );
293 }
294 }
295
296 private function getCurrentFileProtectionHandler(): FileProtectionInterface
297 {
298 if ($this->wordpress->isNginx() === true) {
299 return $this->fileProtectionFactory->createNginxFileProtection();
300 }
301
302 return $this->fileProtectionFactory->createApacheFileProtection();
303 }
304
305 public function getFileProtectionFileName(): string
306 {
307 return $this->getCurrentFileProtectionHandler()->getFileNameWithPath(
308 $this->wordpressConfig->getUploadDirectory()
309 );
310 }
311
312 public function createFileProtection(?string $dir = null, ?string $objectType = null): bool
313 {
314 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
315
316 if ($dir !== null) {
317 return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
318 }
319
320 return false;
321 }
322
323 public function deleteFileProtection(?string $dir = null): bool
324 {
325 $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
326
327 if ($dir !== null) {
328 return $this->getCurrentFileProtectionHandler()->delete($dir);
329 }
330
331 return false;
332 }
333
334 #[NoReturn]
335 public function deliverXSendFileTestFile(): void
336 {
337 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
338 file_put_contents($file, 'success');
339
340 $this->php->header("X-Sendfile: $file");
341 $this->php->header('Content-Type: application/octet-stream');
342 $this->php->header('Content-Disposition: attachment; filename="' . basename($file) . '"');
343 $this->php->callExit();
344 }
345
346 public function removeXSendFileTestFile(): void
347 {
348 $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
349
350 if ($this->php->isFile($file) === true) {
351 $this->php->unlink($file);
352 }
353 }
354 }
355