|
9 | 9 | use ArrayAccess\TrayDigita\Kernel\Decorator; |
10 | 10 | use ArrayAccess\TrayDigita\Kernel\Interfaces\KernelInterface; |
11 | 11 | use ArrayAccess\TrayDigita\Util\Filter\ContainerHelper; |
| 12 | +use ArrayAccess\TrayDigita\Util\Filter\MimeType; |
12 | 13 | use Psr\Http\Message\ServerRequestFactoryInterface; |
13 | 14 | use Psr\Http\Message\StreamFactoryInterface; |
14 | 15 | use RuntimeException; |
|
42 | 43 |
|
43 | 44 | final class Web |
44 | 45 | { |
| 46 | + /** |
| 47 | + * @var string[] $blackListedExtensions List of blacklisted extensions |
| 48 | + */ |
| 49 | + protected static array $blackListedExtensions = [ |
| 50 | + 'php', |
| 51 | + 'cgi', |
| 52 | + 'pl', |
| 53 | + 'py', |
| 54 | + 'rb', |
| 55 | + 'sh', |
| 56 | + 'jsp', |
| 57 | + 'asp', |
| 58 | + 'aspx', |
| 59 | + 'cfm', |
| 60 | + ]; |
| 61 | + |
45 | 62 | // no construct |
46 | 63 | final private function __construct() |
47 | 64 | { |
48 | 65 | } |
49 | 66 |
|
| 67 | + /** |
| 68 | + * Get the blacklisted extensions |
| 69 | + * |
| 70 | + * @return string[] |
| 71 | + */ |
| 72 | + public static function getBlackListedExtensions(): array |
| 73 | + { |
| 74 | + return self::$blackListedExtensions; |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * Check if the extension is blacklisted |
| 79 | + * |
| 80 | + * @param string $extension |
| 81 | + * @return bool |
| 82 | + */ |
| 83 | + public static function isExtensionBlackListed(string $extension): bool |
| 84 | + { |
| 85 | + return in_array(strtolower($extension), self::getBlackListedExtensions(), true); |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * Set the blacklisted extensions |
| 90 | + * |
| 91 | + * @param array<string> $blackListedExtensions |
| 92 | + */ |
| 93 | + public static function setBlackListedExtensions(array $blackListedExtensions): void |
| 94 | + { |
| 95 | + $blackListedExtensions = array_filter($blackListedExtensions, 'is_string'); |
| 96 | + $blackListedExtensions = array_map('strtolower', $blackListedExtensions); |
| 97 | + $blackListedExtensions = array_values(array_filter(array_unique($blackListedExtensions))); |
| 98 | + if (!in_array('php', $blackListedExtensions, true)) { |
| 99 | + // always disallow php |
| 100 | + $blackListedExtensions[] = 'php'; |
| 101 | + } |
| 102 | + self::$blackListedExtensions = $blackListedExtensions; |
| 103 | + } |
| 104 | + |
50 | 105 | /** |
51 | 106 | * @return \Psr\Http\Message\ResponseInterface|false |
52 | 107 | * @noinspection PhpMissingReturnTypeInspection |
@@ -125,26 +180,53 @@ final public static function serve() |
125 | 180 |
|
126 | 181 | $requestUri = $_SERVER['REQUEST_URI']; |
127 | 182 | $requestUriNoQuery = explode('?', $requestUri, 2)[0]; |
| 183 | + $originalScriptFileName = $_SERVER['SCRIPT_FILENAME']; |
| 184 | + $originalScriptName = $_SERVER['SCRIPT_NAME']??$_SERVER['PHP_SELF']??null; |
| 185 | + $isScriptFileNameMatch = $originalScriptFileName === __FILE__; |
| 186 | + $baseName = basename($requestUriNoQuery); |
| 187 | + $extension = str_contains('.', $baseName) |
| 188 | + ? pathinfo($baseName, PATHINFO_EXTENSION) |
| 189 | + : null; |
| 190 | + $extension = $extension && preg_match('~^[a-zA-Z]+$~', $extension) |
| 191 | + ? strtolower($extension) |
| 192 | + : null; |
128 | 193 |
|
129 | 194 | /** |
130 | 195 | * If extension matches & exists return false |
131 | 196 | * that means the builtin web server will serve static assets |
132 | 197 | */ |
133 | 198 | // check mimetypes |
134 | | - if (preg_match( |
135 | | - '~\.(?: |
136 | | - ics|ico|je|jpe?g|avif|heic|png|gif|webp|svg|tiff?|bmp # image |
137 | | - |css|jsx?|x?html?|xml|xsl|xsd|ja?son # web assets |
138 | | - |te?xt|docx?|pptx?|xlsx?|csv|pdf|swf|pps|txt # document |
139 | | - |mp[34]|og[gvpa]|mpe?g|3gp|avi|mov|flac|flv|webm|wmv # media |
140 | | - )$~ix', |
141 | | - $requestUriNoQuery |
142 | | - ) && is_file($publicDirectory . '/' . $requestUriNoQuery)) { |
| 199 | + if ($extension && self::isExtensionBlackListed($extension) |
| 200 | + && is_file($publicDirectory . '/' . $requestUriNoQuery) |
| 201 | + && is_readable($publicDirectory . '/' . $requestUriNoQuery) |
| 202 | + ) { |
| 203 | + $realPath = realpath($publicDirectory . '/' . $requestUriNoQuery); |
| 204 | + $realPathOriginal = $originalScriptName |
| 205 | + ? realpath($publicDirectory . '/' . $originalScriptName) |
| 206 | + : null; |
| 207 | + if (!headers_sent()) { |
| 208 | + $mimeType = MimeType::extension($extension) ?: 'application/octet-stream'; |
| 209 | + header('Content-Type: ' . $mimeType); |
| 210 | + } |
| 211 | + // if the script file name matches the original script file name |
| 212 | + if (!$isScriptFileNameMatch |
| 213 | + && $realPath |
| 214 | + && ( |
| 215 | + $realPath === $originalScriptFileName |
| 216 | + || ( |
| 217 | + // resolve the real path of the original script name |
| 218 | + $originalScriptName && $realPath === $realPathOriginal |
| 219 | + ) |
| 220 | + ) |
| 221 | + ) { |
| 222 | + readfile($realPath); |
| 223 | + } |
| 224 | + |
143 | 225 | // serve the static assets with return : false |
144 | 226 | return $lastResult = false; |
145 | 227 | } |
146 | 228 |
|
147 | | - $_SERVER['PHP_SELF'] = '/' . basename(__FILE__); |
| 229 | + $_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR. basename(__FILE__); |
148 | 230 | $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; |
149 | 231 | } |
150 | 232 |
|
@@ -235,16 +317,16 @@ class_alias('Exception', 'Throwable'); |
235 | 317 | $config = ContainerHelper::use(Config::class, $kernel->getHttpKernel()->getContainer()); |
236 | 318 | $config = $config->get('environment'); |
237 | 319 | $enable = $config instanceof Config && ( |
238 | | - $config->get('displayErrorDetails') === true |
239 | | - || $config->get('debug') === true |
240 | | - ); |
| 320 | + $config->get('displayErrorDetails') === true |
| 321 | + || $config->get('debug') === true |
| 322 | + ); |
241 | 323 | } |
242 | 324 | $additionalText = ! $enable ? "<p><code>$message</code></p>" : <<<HTML |
243 | 325 | <p><code>$message</code></p> |
244 | 326 | <p><code>$file:($line)</code></p> |
245 | 327 | <pre>$trace</pre> |
246 | 328 | HTML; |
247 | | - echo <<<HTML |
| 329 | + echo <<<HTML |
248 | 330 | <!DOCTYPE html> |
249 | 331 | <html lang="en"> |
250 | 332 | <head> |
|
0 commit comments