-
-
Notifications
You must be signed in to change notification settings - Fork 52
/
FilesystemHandler.php
130 lines (110 loc) · 4.31 KB
/
FilesystemHandler.php
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
<?php
namespace FrameworkX;
use FrameworkX\Io\HtmlHandler;
use FrameworkX\Io\RedirectHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
class FilesystemHandler
{
/** @var string */
private $root;
/**
* Mapping between file extension and MIME type to send in `Content-Type` response header
*
* @var array<string,string>
*/
private $mimetypes = array(
'atom' => 'application/atom+xml',
'bz2' => 'application/x-bzip2',
'css' => 'text/css',
'gif' => 'image/gif',
'gz' => 'application/gzip',
'htm' => 'text/html',
'html' => 'text/html',
'ico' => 'image/x-icon',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'js' => 'text/javascript',
'json' => 'application/json',
'pdf' => 'application/pdf',
'png' => 'image/png',
'rss' => 'application/rss+xml',
'svg' => 'image/svg+xml',
'tar' => 'application/x-tar',
'xml' => 'application/xml',
'zip' => 'application/zip',
);
/**
* Assign default MIME type to send in `Content-Type` response header (same as nginx/Apache)
*
* @var string
* @see self::$mimetypes
*/
private $defaultMimetype = 'text/plain';
/** @var ErrorHandler */
private $errorHandler;
/** @var HtmlHandler */
private $html;
public function __construct(string $root)
{
$this->root = $root;
$this->errorHandler = new ErrorHandler();
$this->html = new HtmlHandler();
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$local = $request->getAttribute('path', '');
assert(\is_string($local));
$path = \rtrim($this->root . '/' . $local, '/');
// local path should not contain "./", "../", "//" or null bytes or start with slash
$valid = !\preg_match('#(?:^|/)\.\.?(?:$|/)|^/|//|\x00#', $local);
\clearstatcache();
if ($valid && \is_dir($path)) {
if ($local !== '' && \substr($local, -1) !== '/') {
return (new RedirectHandler(\basename($path) . '/'))();
}
$response = '<strong>' . $this->html->escape($local === '' ? '/' : $local) . '</strong>' . "\n<ul>\n";
if ($local !== '') {
$response .= ' <li><a href="../">../</a></li>' . "\n";
}
$files = \scandir($path);
// @phpstan-ignore-next-line TODO handle error if directory can not be accessed
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$dir = \is_dir($path . '/' . $file) ? '/' : '';
$response .= ' <li><a href="' . \rawurlencode($file) . $dir . '">' . $this->html->escape($file) . $dir . '</a></li>' . "\n";
}
$response .= '</ul>' . "\n";
return Response::html(
$response
);
} elseif ($valid && \is_file($path)) {
if ($local !== '' && \substr($local, -1) === '/') {
return (new RedirectHandler('../' . \basename($path)))();
}
// Assign MIME type based on file extension (same as nginx/Apache) or fall back to given default otherwise.
// Browsers are pretty good at figuring out the correct type if no charset attribute is given.
$ext = \strtolower(\substr($path, \strrpos($path, '.') + 1));
$headers = [
'Content-Type' => $this->mimetypes[$ext] ?? $this->defaultMimetype
];
$stat = @\stat($path);
if ($stat !== false) {
$headers['Last-Modified'] = \gmdate('D, d M Y H:i:s', $stat['mtime']) . ' GMT';
if ($request->getHeaderLine('If-Modified-Since') === $headers['Last-Modified']) {
return new Response(Response::STATUS_NOT_MODIFIED);
}
}
return new Response(
Response::STATUS_OK,
$headers,
\file_get_contents($path) // @phpstan-ignore-line TODO handle error if file can not be accessed
);
} else {
return $this->errorHandler->requestNotFound();
}
}
}