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