Repository navigation
Expand file tree
/
Copy pathStorageController.php
More file actions
145 lines (123 loc) · 4.34 KB
/
Copy pathStorageController.php
File metadata and controls
145 lines (123 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php
namespace ProcessMaker\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Mime\MimeTypes;
class StorageController extends Controller
{
private const CACHE_MAX_AGE = 604800; // 1 week in seconds
/**
* Serve files from the public storage disk.
* This controller handles all storage requests, including tenant paths.
*
* @param Request $request
* @param string $path The path to the file in storage/app/public
*
* @return BinaryFileResponse
* @throws NotFoundHttpException
* @throws AccessDeniedHttpException
*/
public function serve(Request $request, string $path): BinaryFileResponse
{
try {
// Validate and sanitize the path
$path = $this->validateAndSanitizePath($path);
// Get tenant context
$tenant = app()->bound('currentTenant') ? app('currentTenant') : null;
// Get storage disk
// If there were no tenant, the default storage disk is used
$disk = Storage::disk('public');
// Validate file exists and is accessible
if (!$this->validateFile($disk, $path)) {
throw new NotFoundHttpException('File not found or not accessible');
}
// Get file metadata
$metadata = $this->getFileMetadata($disk, $path);
return $this->createFileResponse($metadata);
} catch (NotFoundHttpException | AccessDeniedHttpException $e) {
throw $e;
} catch (\Exception $e) {
Log::error('Error serving file', [
'path' => $path,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw new NotFoundHttpException('Error serving file');
}
}
/**
* Validate and sanitize the file path
*
* @param string $path
*
* @return string
* @throws NotFoundHttpException
*/
private function validateAndSanitizePath(string $path): string
{
// Remove any directory traversal attempts
$path = str_replace(['..', './'], '', $path);
// Ensure path is not empty and doesn't start with a slash
$path = trim($path, '/');
if (empty($path)) {
throw new NotFoundHttpException('Invalid file path');
}
return $path;
}
/**
* Validate file exists and is accessible
*
* @param \Illuminate\Filesystem\FilesystemAdapter $disk
* @param string $path
*
* @return bool
*/
private function validateFile($disk, string $path): bool
{
if (!$disk->exists($path)) {
return false;
}
$fullPath = $disk->path($path);
return is_file($fullPath) && is_readable($fullPath);
}
/**
* Get file metadata
*
* @param \Illuminate\Filesystem\FilesystemAdapter $disk
* @param string $path
*
* @return array
*/
private function getFileMetadata($disk, string $path): array
{
$fullPath = $disk->path($path);
$mimeTypes = new MimeTypes();
return [
'path' => $fullPath,
'mime_type' => $mimeTypes->guessMimeType($fullPath) ?? 'application/octet-stream',
'size' => $disk->size($path),
'last_modified' => $disk->lastModified($path),
];
}
/**
* Create file response with proper headers
*/
private function createFileResponse(array $metadata): BinaryFileResponse
{
$response = response()->file($metadata['path'], [
'Content-Type' => $metadata['mime_type'],
'Content-Length' => $metadata['size'],
'Last-Modified' => gmdate('D, d M Y H:i:s', $metadata['last_modified']) . ' GMT',
'Cache-Control' => 'public, max-age=' . self::CACHE_MAX_AGE,
]);
// Add security headers
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Content-Disposition', 'inline');
return $response;
}
}