PluginProbe
User Access Manager / 2.2.13
User Access Manager v2.2.13
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 +289 -170 trunk2.2.13 View file →
@@ -1,32 +1,88 @@
1 1 <?php
2 +/**
3 + * FileHandler.php
4 + *
5 + * The FileHandler class file.
6 + *
7 + * PHP versions 5
8 + *
9 + * @author Alexander Schneider <alexanderschneider85@gmail.com>
10 + * @copyright 2008-2017 Alexander Schneider
11 + * @license http://www.gnu.org/licenses/gpl-2.0.html GNU General Public License, version 2
12 + * @version SVN: $id$
13 + * @link http://wordpress.org/extend/plugins/user-access-manager/
14 + */
2 15
3 16 declare(strict_types=1);
4 17
5 18 namespace UserAccessManager\File;
6 19
7 -use JetBrains\PhpStorm\NoReturn;
8 20 use UserAccessManager\Config\MainConfig;
9 21 use UserAccessManager\Config\WordpressConfig;
10 -use UserAccessManager\File\Protection\FileProtectionFactory;
11 -use UserAccessManager\File\Protection\FileProtectionInterface;
12 22 use UserAccessManager\Wrapper\Php;
13 23 use UserAccessManager\Wrapper\Wordpress;
14 24
25 +/**
26 + * Class FileHandler
27 + *
28 + * @package UserAccessManager\FileHandler
29 + */
15 30 class FileHandler
16 31 {
17 - public const X_SEND_FILE_TEST_FILE = 'xSendFileTestFile';
32 + const X_SEND_FILE_TEST_FILE = 'xSendFileTestFile';
18 33
34 + /**
35 + * @var Php
36 + */
37 + private $php;
38 +
39 + /**
40 + * @var Wordpress
41 + */
42 + private $wordpress;
43 +
44 + /**
45 + * @var WordpressConfig
46 + */
47 + private $wordpressConfig;
48 +
49 + /**
50 + * @var MainConfig
51 + */
52 + private $mainConfig;
53 +
54 + /**
55 + * @var FileProtectionFactory
56 + */
57 + private $fileProtectionFactory;
58 +
59 + /**
60 + * FileHandler constructor.
61 + * @param Php $php
62 + * @param Wordpress $wordpress
63 + * @param WordpressConfig $wordpressConfig
64 + * @param MainConfig $mainConfig
65 + * @param FileProtectionFactory $fileProtectionFactory
66 + */
19 67 public function __construct(
20 - private Php $php,
21 - private Wordpress $wordpress,
22 - private WordpressConfig $wordpressConfig,
23 - private MainConfig $mainConfig,
24 - private FileProtectionFactory $fileProtectionFactory
68 + Php $php,
69 + Wordpress $wordpress,
70 + WordpressConfig $wordpressConfig,
71 + MainConfig $mainConfig,
72 + FileProtectionFactory $fileProtectionFactory
25 73 ) {
74 + $this->php = $php;
75 + $this->wordpress = $wordpress;
76 + $this->wordpressConfig = $wordpressConfig;
77 + $this->mainConfig = $mainConfig;
78 + $this->fileProtectionFactory = $fileProtectionFactory;
26 79 }
27 80
28 - private function clearBuffer(): void
81 + /**
82 + * Clears the buffer.
83 + */
84 + private function clearBuffer()
29 85 {
30 86 //prevent '\n' / '0A'
31 87 if ((int) $this->php->iniGet('output_buffering') === 0
32 88 && is_numeric(ob_get_length()) === true
@@ -33,43 +89,67 @@
33 89 ) {
34 90 ob_clean();
35 91 }
36 92
37 - $this->php->flush();
93 + flush();
38 94 }
39 95
40 - private function getFileMimeType(string $file): string
96 + /**
97 + * Returns the file mine type.
98 + * @param string $file
99 + * @return string
100 + */
101 + private function getFileMineType(string $file): string
41 102 {
42 - $explodedFileName = explode('.', basename($file));
43 - $fileExtension = strtolower(array_pop($explodedFileName));
103 + $fileName = basename($file);
104 +
105 + /*
106 + * This only for compatibility
107 + * mime_content_type has been deprecated as the PECL extension file info
108 + * provides the same functionality (and more) in a much cleaner way.
109 + */
110 + $explodedFileName = explode('.', $fileName);
111 + $lastElement = array_pop($explodedFileName);
112 + $fileExt = strtolower($lastElement);
113 +
44 114 $mimeTypes = $this->wordpressConfig->getMimeTypes();
45 115
46 - // The deprecated mime_content_type() is only kept as a fallback for installations without fileinfo.
47 116 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);
117 + $fileInfo = finfo_open(FILEINFO_MIME);
118 + $fileMimeType = finfo_file($fileInfo, $file);
119 + finfo_close($fileInfo);
51 120 } elseif ($this->php->functionExists('mime_content_type')) {
52 - $fileMimeType = $this->php->mimeContentType($file);
121 + $fileMimeType = mime_content_type($file);
122 + } elseif (isset($mimeTypes[$fileExt]) === true) {
123 + $fileMimeType = $mimeTypes[$fileExt];
53 124 } else {
54 - $fileMimeType = $mimeTypes[$fileExtension] ?? 'application/octet-stream';
125 + $fileMimeType = 'application/octet-stream';
55 126 }
56 127
57 128 return (string) $fileMimeType;
58 129 }
59 130
60 - private function addDefaultHeader(string $file, bool $isInline): void
131 + /**
132 + * Adds the default header.
133 + * @param string $file
134 + * @param bool $isInline
135 + */
136 + private function addDefaultHeader(string $file, bool $isInline)
61 137 {
62 - $fileMimeType = $this->getFileMimeType($file);
138 + $fileMimeType = $this->getFileMineType($file);
63 139 $contentDisposition = ($isInline === true) ? 'inline' : 'attachment';
64 140 $baseName = str_replace(' ', '_', basename($file));
65 141
66 - $this->php->header('Content-Description: File Transfer');
67 - $this->php->header('Content-Type: ' . $fileMimeType);
68 - $this->php->header("Content-Disposition: $contentDisposition; filename=\"$baseName\"");
142 + header('Content-Description: File Transfer');
143 + header('Content-Type: ' . $fileMimeType);
144 + header("Content-Disposition: {$contentDisposition}; filename=\"{$baseName}\"");
69 145 }
70 146
71 - private function deliverFileViaFopen(string $file): void
147 + /**
148 + * Delivers the file via fopen.
149 + * @param string $file
150 + */
151 + private function deliverFileViaFopen(string $file)
72 152 {
73 153 $handler = fopen($file, 'r');
74 154
75 155 while (feof($handler) === false) {
@@ -80,60 +160,47 @@
80 160 echo $this->php->fread($handler, 1024);
81 161 }
82 162 }
83 163
84 - private function addXSendFileHeader(string $file): bool
164 + /**
165 + * Delivers the file.
166 + * @param string $file
167 + * @param bool $isInline
168 + */
169 + private function deliverFile(string $file, bool $isInline)
85 170 {
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 - private function deliverFile(string $file, bool $isInline): void
105 - {
106 - $this->php->header("HTTP/1.1 200 OK");
107 171 $downloadType = $this->mainConfig->getDownloadType();
108 172
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';
173 + if ($downloadType === 'xsendfile') {
174 + header("X-Sendfile: {$file}");
112 175 }
113 176
114 177 $this->addDefaultHeader($file, $isInline);
115 178
116 - if ($downloadType === 'xsendfile') {
117 - return;
118 - }
179 + if ($downloadType !== 'xsendfile') {
180 + header('Content-Transfer-Encoding: binary');
181 + header('Content-Length: ' . filesize($file));
182 + $this->clearBuffer();
119 183
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);
184 + if ($downloadType === 'fopen') {
185 + $this->deliverFileViaFopen($file);
186 + } else {
187 + readfile($file);
188 + }
128 189 }
129 190 }
130 191
131 192 /**
132 - * Returns the [start, end] byte offsets of a single HTTP range, or null if the range is invalid.
193 + * Sets the seek start and end.
194 + * @param string $range
195 + * @param int $fileSize
196 + * @param int|null $seekStart
197 + * @param int|null $seekEnd
198 + * @return bool
133 199 */
134 - private function getSeekStartEnd(string $range, int $fileSize): ?array
200 + private function getSeekStartEnd(string $range, int $fileSize, ?int &$seekStart, ?int &$seekEnd): bool
135 201 {
202 + //Figure out download piece from range (if set)
136 203 $seek = explode('-', $range);
137 204 $seekStart = ($seek[0] !== '') ? abs((int) $seek[0]) : null;
138 205 $seekEnd = (isset($seek[1]) === true && $seek[1] !== '') ? abs((int) $seek[1]) : null;
139 206 $maxSize = $fileSize - 1;
@@ -140,16 +207,24 @@
140 207
141 208 if ($seekStart === null) {
142 209 $seekStart = $fileSize - $seekEnd;
143 210 $seekEnd = $maxSize;
211 + } elseif ($seekEnd === null) {
212 + $seekEnd = $maxSize;
144 213 }
145 214
146 - $seekEnd = min($seekEnd ?? $maxSize, $maxSize);
215 + //Start and end based on range (if set), else set defaults also check for invalid ranges.
216 + $seekEnd = min($seekEnd, $maxSize);
147 217
148 - return ($seekStart < $seekEnd) ? [$seekStart, $seekEnd] : null;
218 + return $seekStart < $seekEnd;
149 219 }
150 220
151 - private function readFilePartly($fileHandler, int $bytes): void
221 + /**
222 + * Reads the file partly.
223 + * @param resource $fileHandler
224 + * @param int $bytes
225 + */
226 + private function readFilePartly($fileHandler, int $bytes)
152 227 {
153 228 $bytesLeft = $bytes;
154 229 $bufferSize = 1024;
155 230
@@ -165,106 +240,126 @@
165 240 }
166 241 }
167 242 }
168 243
244 + /**
245 + * Returns the http ranges.
246 + * @param int $fileSize
247 + * @return array
248 + */
169 249 private function getRanges(int $fileSize): array
170 250 {
171 251 $httpRange = explode('=', $_SERVER['HTTP_RANGE']);
172 -
173 - if ($httpRange[0] !== 'bytes') {
174 - return [];
175 - }
176 -
252 + $originRanges = isset($httpRange[1]) === true ? $httpRange[1] : '';
253 + $originRanges = explode(',', $originRanges);
254 + $sizeUnit = $httpRange[0];
177 255 $ranges = [];
178 256
179 - foreach (explode(',', $httpRange[1] ?? '') as $originRange) {
180 - $range = $this->getSeekStartEnd($originRange, $fileSize);
257 + if ($sizeUnit === 'bytes') {
258 + foreach ($originRanges as $originRange) {
259 + if ($this->getSeekStartEnd($originRange, $fileSize, $seekStart, $seekEnd) === false) {
260 + $ranges = [];
261 + break;
262 + }
181 263
182 - if ($range === null) {
183 - return [];
264 + $ranges[] = [$seekStart, $seekEnd];
184 265 }
185 -
186 - $ranges[] = $range;
187 266 }
188 267
189 268 return $ranges;
190 269 }
191 270
271 + /**
272 + * Returns the extra contents.
273 + * @param string $file
274 + * @param array $ranges
275 + * @param int|null $contentLength
276 + * @param string|null $boundary
277 + * @return array
278 + */
192 279 private function getExtraContents(string $file, array $ranges, ?int &$contentLength, ?string &$boundary): array
193 280 {
194 281 $contentLength = 0;
195 282 $extraContents = [];
196 283
197 - if (count($ranges) <= 1) {
198 - return $extraContents;
199 - }
284 + //More than one range is requested?
285 + if (count($ranges) > 1) {
286 + $boundary = 'g45d64df96bmdf4sdgh45hf5';
287 + $fullBoundary = "\r\n--{$boundary}--\r\n";
288 + $fileSize = filesize($file);
289 + $mineType = $this->getFileMineType($file);
200 290
201 - $boundary = 'g45d64df96bmdf4sdgh45hf5';
202 - $fullBoundary = "\r\n--$boundary--\r\n";
203 - $fileSize = filesize($file);
204 - $mimeType = $this->getFileMimeType($file);
291 + //compute content length
292 + foreach ($ranges as $index => $range) {
293 + list($seekStart, $seekEnd) = $range;
294 + $extraContent = $fullBoundary;
295 + $extraContent .= "Content-Type: {$mineType}\r\n";
296 + $extraContent .= "Content-Range: bytes $seekStart-$seekEnd/$fileSize\r\n\r\n";
297 + $extraContents[$index] = $extraContent;
298 + $contentLength += strlen($extraContent) + ($seekEnd - $seekStart + 1);
299 + }
205 300
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);
301 + $contentLength += strlen($fullBoundary);
302 + $extraContents[] = $fullBoundary;
213 303 }
214 304
215 - $contentLength += strlen($fullBoundary);
216 - $extraContents[] = $fullBoundary;
217 -
218 305 return $extraContents;
219 306 }
220 307
221 - private function deliverFilePartial(string $file, bool $isInline): void
308 + /**
309 + * Delivers the file partial.
310 + * @param string $file
311 + * @param bool $isInline
312 + */
313 + private function deliverFilePartial(string $file, bool $isInline)
222 314 {
223 315 $fileSize = filesize($file);
224 316 $ranges = $this->getRanges($fileSize);
225 317
226 - if ($ranges === []) {
227 - $this->php->header('HTTP/1.1 416 Requested Range Not Satisfiable');
228 - $this->php->header("Content-Range: */$fileSize");
318 + if ($ranges !== []) {
319 + $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
229 320
230 - return;
231 - }
321 + header('HTTP/1.1 206 Partial Content');
322 + header('Content-Transfer-Encoding: binary');
323 + header('Accept-Ranges: bytes');
232 324
233 - $extraContents = $this->getExtraContents($file, $ranges, $contentLength, $boundary);
325 + if ($extraContents === []) {
326 + $this->addDefaultHeader($file, $isInline);
327 + list($seekStart, $seekEnd) = $ranges[0];
328 + $contentLength = ($seekEnd - $seekStart + 1);
329 + header("Content-Range: bytes {$seekStart}-{$seekEnd}/{$fileSize}");
330 + } else {
331 + header("Content-Type: multipart/x-byteranges; boundary={$boundary}");
332 + }
234 333
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');
334 + header("Content-Length: {$contentLength}");
335 + $fileHandler = fopen($file, 'r');
238 336
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 - }
337 + foreach ($ranges as $index => $range) {
338 + if (isset($extraContents[$index]) === true) {
339 + echo $extraContents[$index];
340 + }
247 341
248 - $this->php->header("Content-Length: $contentLength");
249 - $fileHandler = fopen($file, 'r');
342 + list($seekStart, $seekEnd) = $ranges[0];
343 + fseek($fileHandler, $seekStart);
344 + $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
345 + }
250 346
251 - foreach ($ranges as $index => $range) {
252 - if (isset($extraContents[$index]) === true) {
253 - echo $extraContents[$index];
347 + if ($extraContents !== []) {
348 + echo end($extraContents);
349 + $this->clearBuffer();
254 350 }
255 -
256 - [$seekStart, $seekEnd] = $range;
257 - $this->php->fseek($fileHandler, $seekStart);
258 - $this->readFilePartly($fileHandler, $seekEnd - $seekStart + 1);
351 + } else {
352 + header('HTTP/1.1 416 Requested Range Not Satisfiable');
353 + header("Content-Range: */$fileSize");
259 354 }
260 -
261 - if ($extraContents !== []) {
262 - echo end($extraContents);
263 - $this->clearBuffer();
264 - }
265 355 }
266 356
357 + /**
358 + * Checks if the file is an inline file
359 + * @param string $file
360 + * @return bool
361 + */
267 362 private function isInlineFile(string $file): bool
268 363 {
269 364 $inlineFiles = array_map('trim', explode(',', (string) $this->mainConfig->getInlineFiles()));
270 365 $map = array_flip($inlineFiles);
@@ -272,39 +367,42 @@
272 367
273 368 return isset($map[$extension]);
274 369 }
275 370
276 - private function isRangeRequest(): bool
371 + /**
372 + * Delivers the content of the requested file.
373 + * @param string $file
374 + * @param bool $isImage
375 + */
376 + public function getFile(string $file, bool $isImage)
277 377 {
278 - return isset($_SERVER['HTTP_RANGE']) === true
279 - && isset($_SERVER['REQUEST_METHOD']) === true
280 - && $_SERVER['REQUEST_METHOD'] === 'GET';
281 - }
378 + //Deliver content
379 + if (file_exists($file) === true) {
380 + $isInline = $isImage === true || $this->isInlineFile($file) === true;
282 381
283 - #[NoReturn]
284 - public function getFile(string $file, bool $isImage): void
285 - {
286 - if (file_exists($file) === false) {
382 + if (isset($_SERVER['HTTP_RANGE']) === true
383 + && isset($_SERVER['REQUEST_METHOD']) === true
384 + && $_SERVER['REQUEST_METHOD'] === 'GET'
385 + ) {
386 + $this->deliverFilePartial($file, $isInline);
387 + } else {
388 + $this->deliverFile($file, $isInline);
389 + }
390 +
391 + $this->php->callExit();
392 + } else {
287 393 $this->wordpress->wpDie(
288 394 TXT_UAM_FILE_NOT_FOUND_ERROR_MESSAGE,
289 395 TXT_UAM_FILE_NOT_FOUND_ERROR_TITLE,
290 396 ['response' => 404]
291 397 );
292 -
293 - return;
294 398 }
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 399 }
306 400
401 + /**
402 + * Returns the current file protection handler.
403 + * @return FileProtectionInterface
404 + */
307 405 private function getCurrentFileProtectionHandler(): FileProtectionInterface
308 406 {
309 407 if ($this->wordpress->isNginx() === true) {
310 408 return $this->fileProtectionFactory->createNginxFileProtection();
@@ -312,8 +410,12 @@
312 410
313 411 return $this->fileProtectionFactory->createApacheFileProtection();
314 412 }
315 413
414 + /**
415 + * Returns the file protection file.
416 + * @return string
417 + */
316 418 public function getFileProtectionFileName(): string
317 419 {
318 420 return $this->getCurrentFileProtectionHandler()->getFileNameWithPath(
319 421 $this->wordpressConfig->getUploadDirectory()
@@ -319,46 +421,63 @@
319 421 $this->wordpressConfig->getUploadDirectory()
320 422 );
321 423 }
322 424
323 - public function createFileProtection(?string $dir = null, ?string $objectType = null): bool
425 + /**
426 + * Creates a protection file.
427 + * @param string $dir The destination directory.
428 + * @param string $objectType The object type.
429 + * @return false
430 + */
431 + public function createFileProtection($dir = null, $objectType = null): bool
324 432 {
325 - $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
433 + $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
326 434
327 - return $directory !== null
328 - && $this->getCurrentFileProtectionHandler()->create($directory, $objectType);
435 + if ($dir !== null) {
436 + return $this->getCurrentFileProtectionHandler()->create($dir, $objectType);
437 + }
438 +
439 + return false;
329 440 }
330 441
331 - public function deleteFileProtection(?string $dir = null): bool
442 + /**
443 + * Deletes the protection files.
444 + * @param string $dir The destination directory.
445 + * @return false
446 + */
447 + public function deleteFileProtection($dir = null): bool
332 448 {
333 - $directory = $dir ?? $this->wordpressConfig->getUploadDirectory();
449 + $dir = ($dir === null) ? $this->wordpressConfig->getUploadDirectory() : $dir;
334 450
335 - return $directory !== null
336 - && $this->getCurrentFileProtectionHandler()->delete($directory);
337 - }
451 + if ($dir !== null) {
452 + return $this->getCurrentFileProtectionHandler()->delete($dir);
453 + }
338 454
339 - private function getXSendFileTestFilePath(): string
340 - {
341 - return $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
455 + return false;
342 456 }
343 457
344 - #[NoReturn]
345 - public function deliverXSendFileTestFile(): void
458 + /**
459 + * Delivers a xsendfile test file.
460 + */
461 + public function deliverXSendFileTestFile()
346 462 {
347 - $file = $this->getXSendFileTestFilePath();
463 + $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
348 464 file_put_contents($file, 'success');
349 465
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) . '"');
466 + header("X-Sendfile: {$file}");
467 + header('Content-Type: application/octet-stream');
468 + header('Content-Disposition: attachment; filename="' . basename($file) . '"');
353 469 $this->php->callExit();
354 470 }
355 471
356 - public function removeXSendFileTestFile(): void
472 + /**
473 + * Removes the xsendfile test file if exists.
474 + */
475 + public function removeXSendFileTestFile()
357 476 {
358 - $file = $this->getXSendFileTestFilePath();
477 + $file = $this->wordpressConfig->getUploadDirectory() . DIRECTORY_SEPARATOR . self::X_SEND_FILE_TEST_FILE;
359 478
360 - if ($this->php->isFile($file) === true) {
361 - $this->php->unlink($file);
479 + if (file_exists($file) === true) {
480 + unlink($file);
362 481 }
363 482 }
364 483 }