PluginProbe
User Access Manager / trunk
User Access Manager vtrunk
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
← All changes | src/File/FileHandler.php +153 -143 2.3.14trunk View file →
@@ -6,8 +6,10 @@
6 6
7 7 use JetBrains\PhpStorm\NoReturn;
8 8 use UserAccessManager\Config\MainConfig;
9 9 use UserAccessManager\Config\WordpressConfig;
10 +use UserAccessManager\File\Protection\FileProtectionFactory;
11 +use UserAccessManager\File\Protection\FileProtectionInterface;
10 12 use UserAccessManager\Wrapper\Php;
11 13 use UserAccessManager\Wrapper\Wordpress;
12 14
13 15 class FileHandler
@@ -31,36 +33,26 @@
31 33 ) {
32 34 ob_clean();
33 35 }
34 36
35 - flush();
37 + $this->php->flush();
36 38 }
37 39
38 - private function getFileMineType(string $file): string
40 + private function getFileMimeType(string $file): string
39 41 {
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 -
42 + $explodedFileName = explode('.', basename($file));
43 + $fileExtension = strtolower(array_pop($explodedFileName));
51 44 $mimeTypes = $this->wordpressConfig->getMimeTypes();
52 45
46 + // The deprecated mime_content_type() is only kept as a fallback for installations without fileinfo.
53 47 if ($this->php->functionExists('finfo_open') === true) {
54 - $fileInfo = finfo_open(FILEINFO_MIME);
55 - $fileMimeType = finfo_file($fileInfo, $file);
56 - finfo_close($fileInfo);
48 + $fileInfo = $this->php->fInfoOpen(FILEINFO_MIME);
49 + $fileMimeType = $this->php->fInfoFile($fileInfo, $file);
50 + $this->php->fInfoClose($fileInfo);
57 51 } elseif ($this->php->functionExists('mime_content_type')) {
58 - $fileMimeType = mime_content_type($file);
59 - } elseif (isset($mimeTypes[$fileExt]) === true) {
60 - $fileMimeType = $mimeTypes[$fileExt];
52 + $fileMimeType = $this->php->mimeContentType($file);
61 53 } else {
62 - $fileMimeType = 'application/octet-stream';
54 + $fileMimeType = $mimeTypes[$fileExtension] ?? 'application/octet-stream';
63 55 }
64 56
65 57 return (string) $fileMimeType;
66 58 }
@@ -66,15 +58,15 @@
66 58 }
67 59
68 60 private function addDefaultHeader(string $file, bool $isInline): void
69 61 {
70 - $fileMimeType = $this->getFileMineType($file);
62 + $fileMimeType = $this->getFileMimeType($file);
71 63 $contentDisposition = ($isInline === true) ? 'inline' : 'attachment';
72 64 $baseName = str_replace(' ', '_', basename($file));
73 65
74 - header('Content-Description: File Transfer');
75 - header('Content-Type: ' . $fileMimeType);
76 - header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
66 + $this->php->header('Content-Description: File Transfer');
67 + $this->php->header('Content-Type: ' . $fileMimeType);
68 + $this->php->header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
77 69 }
78 70
79 71 private function deliverFileViaFopen(string $file): void
80 72 {
@@ -88,46 +80,60 @@
88 80 echo $this->php->fread($handler, 1024);
89 81 }
90 82 }
91 83
84 + private function addXSendFileHeader(string $file): bool
85 + {
86 + if ($this->wordpress->isNginx()) {
87 + // The /uam-files prefix targets a dedicated internal location that bypasses
88 + // UAM's rewrite rules, which would otherwise make the redirect loop.
89 + $uri = '/uam-files' . str_replace(rtrim(ABSPATH, '/'), '', $file);
90 + $this->php->header("X-Accel-Redirect: $uri");
91 +
92 + return true;
93 + }
94 +
95 + if ($this->wordpress->isApacheModuleLoaded('mod_xsendfile')) {
96 + $this->php->header("X-Sendfile: $file");
97 +
98 + return true;
99 + }
100 +
101 + return false;
102 + }
103 +
92 104 private function deliverFile(string $file, bool $isInline): void
93 105 {
94 - header("HTTP/1.1 200 OK");
106 + $this->php->header("HTTP/1.1 200 OK");
95 107 $downloadType = $this->mainConfig->getDownloadType();
96 108
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 - header("X-Accel-Redirect: $uri");
103 - } elseif ($this->wordpress->isApacheModuleLoaded('mod_xsendfile')) {
104 - 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 - }
109 + if ($downloadType === 'xsendfile' && $this->addXSendFileHeader($file) === false) {
110 + // Without server-side sending support the file still has to be delivered by PHP.
111 + $downloadType = 'fopen';
110 112 }
111 113
112 114 $this->addDefaultHeader($file, $isInline);
113 115
114 - if ($downloadType !== 'xsendfile') {
115 - header('Content-Transfer-Encoding: binary');
116 - header('Content-Length: ' . filesize($file));
117 - $this->clearBuffer();
116 + if ($downloadType === 'xsendfile') {
117 + return;
118 + }
118 119
119 - if ($downloadType === 'fopen') {
120 - $this->deliverFileViaFopen($file);
121 - } else {
122 - readfile($file);
123 - }
120 + $this->php->header('Content-Transfer-Encoding: binary');
121 + $this->php->header('Content-Length: ' . filesize($file));
122 + $this->clearBuffer();
123 +
124 + if ($downloadType === 'fopen') {
125 + $this->deliverFileViaFopen($file);
126 + } else {
127 + readfile($file);
124 128 }
125 129 }
126 130
127 - private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
131 + /**
132 + * Returns the [start, end] byte offsets of a single HTTP range, or null if the range is invalid.
133 + */
134 + private function getSeekStartEnd(string $range, int $fileSize): ?array
128 135 {
129 - //Figure out download piece from range (if set)
130 136 $seek = explode('-', $range);
131 137 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
132 138 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
133 139 $maxSize = $fileSize - 1;
@@ -134,16 +140,13 @@
134 140
135 141 if ($seekStart === null) {
136 142 $seekStart = $fileSize - $seekEnd;
137 143 $seekEnd = $maxSize;
138 - } elseif ($seekEnd === null) {
139 - $seekEnd = $maxSize;
140 144 }
141 145
142 - //Start and end based on range (if set), else set defaults also check for invalid ranges.
143 - $seekEnd = min($seekEnd, $maxSize);
146 + $seekEnd = min($seekEnd ?? $maxSize, $maxSize);
144 147
145 - return $seekStart < $seekEnd;
148 + return ($seekStart < $seekEnd) ? [$seekStart, $seekEnd] : null;
146 149 }
147 150
148 151 private function readFilePartly($fileHandler, int $bytes): void
149 152 {
@@ -165,22 +168,23 @@
165 168
166 169 private function getRanges(int $fileSize): array
167 170 {
168 171 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
169 - $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
170 - $originRanges = explode(',', $originRanges);
171 - $sizeUnit = $httpRange[0];
172 +
173 + if ($httpRange[0] !== 'bytes') {
174 + return [];
175 + }
176 +
172 177 $ranges = [];
173 178
174 - if ($sizeUnit === 'bytes') {
175 - foreach ($originRanges as $originRange) {
176 - if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
177 - $ranges = [];
178 - break;
179 - }
179 + foreach (explode(',', $httpRange[1] ?? '') as $originRange) {
180 + $range = $this->getSeekStartEnd($originRange, $fileSize);
180 181
181 - $ranges[] = [$seekStart, $seekEnd];
182 + if ($range === null) {
183 + return [];
182 184 }
185 +
186 + $ranges[] = $range;
183 187 }
184 188
185 189 return $ranges;
186 190 }
@@ -189,29 +193,29 @@
189 193 {
190 194 $contentLength = 0;
191 195 $extraContents = [];
192 196
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);
197 + if (count($ranges) <= 1) {
198 + return $extraContents;
199 + }
199 200
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 - }
201 + $boundary = 'g45d64df96bmdf4sdgh45hf5';
202 + $fullBoundary = "\r\n--$boundary--\r\n";
203 + $fileSize = filesize($file);
204 + $mimeType = $this->getFileMimeType($file);
209 205
210 - $contentLength += strlen($fullBoundary);
211 - $extraContents[] = $fullBoundary;
206 + foreach ($ranges as $index => $range) {
207 + [$seekStart, $seekEnd] = $range;
208 + $extraContent = $fullBoundary
209 + . "Content-Type: $mimeType\r\n"
210 + . "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
211 + $extraContents[$index] = $extraContent;
212 + $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
212 213 }
213 214
215 + $contentLength += strlen($fullBoundary);
216 + $extraContents[] = $fullBoundary;
217 +
214 218 return $extraContents;
215 219 }
216 220
217 221 private function deliverFilePartial(string $file, bool $isInline): void
@@ -218,45 +222,47 @@
218 222 {
219 223 $fileSize = filesize($file);
220 224 $ranges = $this->getRanges($fileSize);
221 225
222 - if ($ranges !== []) {
223 - $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
226 + if ($ranges === []) {
227 + $this->php->header('HTTP/1.1 416 Requested Range Not Satisfiable');
228 + $this->php->header("Content-Range: */$fileSize");
224 229
225 - header('HTTP/1.1 206 Partial Content');
226 - header('Content-Transfer-Encoding: binary');
227 - header('Accept-Ranges: bytes');
230 + return;
231 + }
228 232
229 - if ($extraContents === []) {
230 - $this->addDefaultHeader($file, $isInline);
231 - [$seekStart, $seekEnd] = $ranges[0];
232 - $contentLength = ($seekEnd - $seekStart + 1);
233 - header("Content-Range: bytes $seekStart-$seekEnd/$fileSize");
234 - } else {
235 - header("Content-Type: multipart/x-byteranges; boundary=$boundary");
236 - }
233 + $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
237 234
238 - header("Content-Length: $contentLength");
239 - $fileHandler = fopen($file, 'r');
235 + $this->php->header('HTTP/1.1 206 Partial Content');
236 + $this->php->header('Content-Transfer-Encoding: binary');
237 + $this->php->header('Accept-Ranges: bytes');
240 238
241 - foreach ($ranges as $index => $range) {
242 - if (isset($extraContents[$index]) === true) {
243 - echo $extraContents[$index];
244 - }
239 + if ($extraContents === []) {
240 + $this->addDefaultHeader($file, $isInline);
241 + [$seekStart, $seekEnd] = $ranges[0];
242 + $contentLength = ($seekEnd - $seekStart + 1);
243 + $this->php->header("Content-Range: bytes $seekStart-$seekEnd/$fileSize");
244 + } else {
245 + $this->php->header("Content-Type: multipart/x-byteranges; boundary=$boundary");
246 + }
245 247
246 - [$seekStart, $seekEnd] = $ranges[0];
247 - fseek($fileHandler, $seekStart);
248 - $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
248 + $this->php->header("Content-Length: $contentLength");
249 + $fileHandler = fopen($file, 'r');
250 +
251 + foreach ($ranges as $index => $range) {
252 + if (isset($extraContents[$index]) === true) {
253 + echo $extraContents[$index];
249 254 }
250 255
251 - if ($extraContents !== []) {
252 - echo end($extraContents);
253 - $this->clearBuffer();
254 - }
255 - } else {
256 - header('HTTP/1.1 416 Requested Range Not Satisfiable');
257 - header("Content-Range: */$fileSize");
256 + [$seekStart, $seekEnd] = $range;
257 + $this->php->fseek($fileHandler, $seekStart);
258 + $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
258 259 }
260 +
261 + if ($extraContents !== []) {
262 + echo end($extraContents);
263 + $this->clearBuffer();
264 + }
259 265 }
260 266
261 267 private function isInlineFile(string $file): bool
262 268 {
@@ -266,32 +272,37 @@
266 272
267 273 return isset($map[$extension]);
268 274 }
269 275
276 + private function isRangeRequest(): bool
277 + {
278 + return isset($_SERVER['HTTP_RANGE']) === true
279 + && isset($_SERVER['REQUEST_METHOD']) === true
280 + && $_SERVER['REQUEST_METHOD'] === 'GET';
281 + }
282 +
270 283 #[NoReturn]
271 284 public function getFile(string $file, bool $isImage): void
272 285 {
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 {
286 + if (file_exists($file) === false) {
288 287 $this->wordpress->wpDie(
289 288 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
290 289 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
291 290 ['response' => 404]
292 291 );
292 +
293 + return;
293 294 }
295 +
296 + $isInline = $isImage === true || $this->isInlineFile($file) === true;
297 +
298 + if ($this->isRangeRequest() === true) {
299 + $this->deliverFilePartial($file, $isInline);
300 + } else {
301 + $this->deliverFile($file, $isInline);
302 + }
303 +
304 + $this->php->callExit();
294 305 }
295 306
296 307 private function getCurrentFileProtectionHandler(): FileProtectionInterface
297 308 {
@@ -310,45 +321,44 @@
310 321 }
311 322
312 323 public function createFileProtection(?string $dir = null, ?string $objectType = null): bool
313 324 {
314 - $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
325 + $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
315 326
316 - if ($dir !== null) {
317 - return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
318 - }
319 -
320 - return false;
327 + return $directory !== null
328 + && $this->getCurrentFileProtectionHandler()->create($directory, $objectType);
321 329 }
322 330
323 331 public function deleteFileProtection(?string $dir = null): bool
324 332 {
325 - $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
333 + $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
326 334
327 - if ($dir !== null) {
328 - return $this->getCurrentFileProtectionHandler()->delete($dir);
329 - }
335 + return $directory !== null
336 + && $this->getCurrentFileProtectionHandler()->delete($directory);
337 + }
330 338
331 - return false;
339 + private function getXSendFileTestFilePath(): string
340 + {
341 + return $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
332 342 }
333 343
334 344 #[NoReturn]
335 345 public function deliverXSendFileTestFile(): void
336 346 {
337 - $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
347 + $file = $this->getXSendFileTestFilePath();
338 348 file_put_contents($file, 'success');
339 349
340 - header("X-Sendfile: $file");
341 - header('Content-Type: application/octet-stream');
342 - header('Content-Disposition: attachment; filename="' . basename($file) . '"');
350 + $this->php->header("X-Sendfile: $file");
351 + $this->php->header('Content-Type: application/octet-stream');
352 + $this->php->header('Content-Disposition: attachment; filename="' . basename($file) . '"');
343 353 $this->php->callExit();
344 354 }
345 355
346 356 public function removeXSendFileTestFile(): void
347 357 {
348 - $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
358 + $file = $this->getXSendFileTestFilePath();
349 359
350 - if (file_exists($file) === true) {
351 - unlink($file);
360 + if ($this->php->isFile($file) === true) {
361 + $this->php->unlink($file);
352 362 }
353 363 }
354 364 }