PluginProbe
User Access Manager / 2.3.4
User Access Manager v2.3.4
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 +134 -155 2.3.182.3.4 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
@@ -33,26 +31,36 @@
33 31 ) {
34 32 ob_clean();
35 33 }
36 34
37 - $this->php->flush();
35 + 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);
50 - $this->php->fInfoClose($fileInfo);
54 + $fileInfo = finfo_open(FILEINFO_MIME);
55 + $fileMimeType = finfo_file($fileInfo, $file);
56 + finfo_close($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,15 +66,15 @@
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 - $this->php->header('Content-Description: File Transfer');
67 - $this->php->header('Content-Type: ' . $fileMimeType);
68 - $this->php->header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
74 + header('Content-Description: File Transfer');
75 + header('Content-Type: ' . $fileMimeType);
76 + header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
69 77 }
70 78
71 79 private function deliverFileViaFopen(string $file): void
72 80 {
@@ -80,60 +88,35 @@
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 - $this->php->header("HTTP/1.1 200 OK");
94 + 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 + header("X-Sendfile: $file");
112 99 }
113 100
114 101 $this->addDefaultHeader($file, $isInline);
115 102
116 - if ($downloadType === 'xsendfile') {
117 - return;
118 - }
103 + if ($downloadType !== 'xsendfile') {
104 + header('Content-Transfer-Encoding: binary');
105 + header('Content-Length: ' . filesize($file));
106 + $this->clearBuffer();
119 107
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);
108 + if ($downloadType === 'fopen') {
109 + $this->deliverFileViaFopen($file);
110 + } else {
111 + readfile($file);
112 + }
128 113 }
129 114 }
130 115
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
116 + private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
135 117 {
118 + //Figure out download piece from range (if set)
136 119 $seek = explode('-', $range);
137 120 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
138 121 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
139 122 $maxSize = $fileSize - 1;
@@ -140,13 +123,16 @@
140 123
141 124 if ($seekStart === null) {
142 125 $seekStart = $fileSize - $seekEnd;
143 126 $seekEnd = $maxSize;
127 + } elseif ($seekEnd === null) {
128 + $seekEnd = $maxSize;
144 129 }
145 130
146 - $seekEnd = min($seekEnd ?? $maxSize, $maxSize);
131 + //Start and end based on range (if set), else set defaults also check for invalid ranges.
132 + $seekEnd = min($seekEnd, $maxSize);
147 133
148 - return ($seekStart < $seekEnd) ? [$seekStart, $seekEnd] : null;
134 + return $seekStart < $seekEnd;
149 135 }
150 136
151 137 private function readFilePartly($fileHandler, int $bytes): void
152 138 {
@@ -168,23 +154,22 @@
168 154
169 155 private function getRanges(int $fileSize): array
170 156 {
171 157 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
172 -
173 - if ($httpRange[0] !== 'bytes') {
174 - return [];
175 - }
176 -
158 + $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
159 + $originRanges = explode(',', $originRanges);
160 + $sizeUnit = $httpRange[0];
177 161 $ranges = [];
178 162
179 - foreach (explode(',', $httpRange[1] ?? '') as $originRange) {
180 - $range = $this->getSeekStartEnd($originRange, $fileSize);
163 + if ($sizeUnit === 'bytes') {
164 + foreach ($originRanges as $originRange) {
165 + if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
166 + $ranges = [];
167 + break;
168 + }
181 169
182 - if ($range === null) {
183 - return [];
170 + $ranges[] = [$seekStart, $seekEnd];
184 171 }
185 -
186 - $ranges[] = $range;
187 172 }
188 173
189 174 return $ranges;
190 175 }
@@ -193,29 +178,29 @@
193 178 {
194 179 $contentLength = 0;
195 180 $extraContents = [];
196 181
197 - if (count($ranges) <= 1) {
198 - return $extraContents;
199 - }
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);
200 188
201 - $boundary = 'g45d64df96bmdf4sdgh45hf5';
202 - $fullBoundary = "\r\n--$boundary--\r\n";
203 - $fileSize = filesize($file);
204 - $mimeType = $this->getFileMimeType($file);
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 + }
205 198
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);
199 + $contentLength += strlen($fullBoundary);
200 + $extraContents[] = $fullBoundary;
213 201 }
214 202
215 - $contentLength += strlen($fullBoundary);
216 - $extraContents[] = $fullBoundary;
217 -
218 203 return $extraContents;
219 204 }
220 205
221 206 private function deliverFilePartial(string $file, bool $isInline): void
@@ -222,47 +207,45 @@
222 207 {
223 208 $fileSize = filesize($file);
224 209 $ranges = $this->getRanges($fileSize);
225 210
226 - if ($ranges === []) {
227 - $this->php->header('HTTP/1.1 416 Requested Range Not Satisfiable');
228 - $this->php->header("Content-Range: */$fileSize");
211 + if ($ranges !== []) {
212 + $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
229 213
230 - return;
231 - }
214 + header('HTTP/1.1 206 Partial Content');
215 + header('Content-Transfer-Encoding: binary');
216 + header('Accept-Ranges: bytes');
232 217
233 - $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
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 + }
234 226
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');
227 + header("Content-Length: $contentLength");
228 + $fileHandler = fopen($file, 'r');
238 229
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 - }
230 + foreach ($ranges as $index => $range) {
231 + if (isset($extraContents[$index]) === true) {
232 + echo $extraContents[$index];
233 + }
247 234
248 - $this->php->header("Content-Length: $contentLength");
249 - $fileHandler = fopen($file, 'r');
235 + [$seekStart, $seekEnd] = $ranges[0];
236 + fseek($fileHandler, $seekStart);
237 + $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
238 + }
250 239
251 - foreach ($ranges as $index => $range) {
252 - if (isset($extraContents[$index]) === true) {
253 - echo $extraContents[$index];
240 + if ($extraContents !== []) {
241 + echo end($extraContents);
242 + $this->clearBuffer();
254 243 }
255 -
256 - [$seekStart, $seekEnd] = $range;
257 - $this->php->fseek($fileHandler, $seekStart);
258 - $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
244 + } else {
245 + header('HTTP/1.1 416 Requested Range Not Satisfiable');
246 + header("Content-Range: */$fileSize");
259 247 }
260 -
261 - if ($extraContents !== []) {
262 - echo end($extraContents);
263 - $this->clearBuffer();
264 - }
265 248 }
266 249
267 250 private function isInlineFile(string $file): bool
268 251 {
@@ -272,37 +255,32 @@
272 255
273 256 return isset($map[$extension]);
274 257 }
275 258
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 259 #[NoReturn]
284 260 public function getFile(string $file, bool $isImage): void
285 261 {
286 - if (file_exists($file) === false) {
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 {
287 277 $this->wordpress->wpDie(
288 278 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
289 279 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
290 280 ['response' => 404]
291 281 );
292 -
293 - return;
294 282 }
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 283 }
306 284
307 285 private function getCurrentFileProtectionHandler(): FileProtectionInterface
308 286 {
@@ -319,46 +297,47 @@
319 297 $this->wordpressConfig->getUploadDirectory()
320 298 );
321 299 }
322 300
323 - public function createFileProtection(?string $dir = null, ?string $objectType = null): bool
301 + public function createFileProtection(string $dir = null, string $objectType = null): bool
324 302 {
325 - $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
303 + $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
326 304
327 - return $directory !== null
328 - && $this->getCurrentFileProtectionHandler()->create($directory, $objectType);
305 + if ($dir !== null) {
306 + return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
307 + }
308 +
309 + return false;
329 310 }
330 311
331 - public function deleteFileProtection(?string $dir = null): bool
312 + public function deleteFileProtection(string $dir = null): bool
332 313 {
333 - $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
314 + $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
334 315
335 - return $directory !== null
336 - && $this->getCurrentFileProtectionHandler()->delete($directory);
337 - }
316 + if ($dir !== null) {
317 + return $this->getCurrentFileProtectionHandler()->delete($dir);
318 + }
338 319
339 - private function getXSendFileTestFilePath(): string
340 - {
341 - return $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
320 + return false;
342 321 }
343 322
344 323 #[NoReturn]
345 324 public function deliverXSendFileTestFile(): void
346 325 {
347 - $file = $this->getXSendFileTestFilePath();
326 + $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
348 327 file_put_contents($file, 'success');
349 328
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) . '"');
329 + header("X-Sendfile: $file");
330 + header('Content-Type: application/octet-stream');
331 + header('Content-Disposition: attachment; filename="' . basename($file) . '"');
353 332 $this->php->callExit();
354 333 }
355 334
356 335 public function removeXSendFileTestFile(): void
357 336 {
358 - $file = $this->getXSendFileTestFilePath();
337 + $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
359 338
360 - if ($this->php->isFile($file) === true) {
361 - $this->php->unlink($file);
339 + if (file_exists($file) === true) {
340 + unlink($file);
362 341 }
363 342 }
364 343 }