169 lines
5.1 KiB
PHP
169 lines
5.1 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace Orbit\Module;
|
|
|
|
use Orbit\Module;
|
|
use Orbit\Request;
|
|
use Orbit\Response;
|
|
|
|
/**
|
|
* Static files server module
|
|
*
|
|
* @uses Module
|
|
* @package Orbit
|
|
*/
|
|
class Statics extends Module
|
|
{
|
|
const WORLD_READABLE = 0x0004;
|
|
|
|
/**
|
|
* Handle a request and generate a proper response
|
|
*
|
|
* @param Request $request The request object
|
|
*/
|
|
public function handle(Request $request): array
|
|
{
|
|
$response = new Response();
|
|
$real_root_dir = realpath($this->config->root_dir);
|
|
$resource_path = rtrim($real_root_dir, "/") . $request->path;
|
|
|
|
// Check if within the server root
|
|
// Realpath will translate any '..' in the path
|
|
$realpath = realpath($resource_path);
|
|
if ($realpath && strpos($realpath, $real_root_dir) !== 0) {
|
|
$response->setStatus(Response::STATUS_PERMANENT_FAILURE);
|
|
$response->setMeta("Invalid location");
|
|
return [false, $response];
|
|
}
|
|
|
|
if (is_dir($resource_path)) {
|
|
// If missing the final slash, issue a redirect
|
|
if ($resource_path[-1] != "/") {
|
|
$response->setStatus(Response::STATUS_REDIRECT_PERMANENT);
|
|
$response->setMeta($request->getUrlAppendPath('/'));
|
|
return [false, $response];
|
|
}
|
|
|
|
// Check if index file exists
|
|
if (file_exists($resource_path . DIRECTORY_SEPARATOR . $this->config->index_file)) {
|
|
$resource_path = $resource_path . DIRECTORY_SEPARATOR . $this->config->index_file;
|
|
} else {
|
|
if (!$this->config->enable_directory_index) {
|
|
$response->setStatus(Response::STATUS_NOT_FOUND);
|
|
$response->setMeta('Path not available');
|
|
return [false, $response];
|
|
} else {
|
|
$response->setStatus(Response::STATUS_SUCCESS);
|
|
$response->setMeta('text/gemini');
|
|
$response->setBody($this->makeDirectoryIndex($resource_path, $real_root_dir));
|
|
return [true, $response];
|
|
}
|
|
}
|
|
}
|
|
|
|
// File exists and is world readable
|
|
if (file_exists($resource_path) && self::isWorldReadable($resource_path)) {
|
|
$response->setStatus(Response::STATUS_SUCCESS);
|
|
|
|
$pathinfo = pathinfo($resource_path);
|
|
|
|
if (isset($pathinfo['extension'])) {
|
|
$meta = $this->getCustomMimeFromFileExtension($pathinfo['extension']);
|
|
if (!$meta) {
|
|
$meta = mime_content_type($resource_path);
|
|
}
|
|
} else {
|
|
// Use finfo_file to detect type
|
|
$meta = finfo_file(finfo_open(FILEINFO_MIME), $resource_path);
|
|
}
|
|
|
|
$response->setMeta($meta);
|
|
$response->setStaticFile($resource_path);
|
|
} else {
|
|
$response->setStatus(Response::STATUS_NOT_FOUND);
|
|
$response->setMeta('Not found!');
|
|
}
|
|
|
|
return [true, $response];
|
|
}
|
|
|
|
/**
|
|
* Get mime type from file extension for custom types
|
|
*
|
|
* @param string $extension
|
|
* @return string
|
|
*/
|
|
public function getCustomMimeFromFileExtension($extension): string
|
|
{
|
|
switch ($extension) {
|
|
case 'gmi':
|
|
case 'gemini':
|
|
return 'text/gemini';
|
|
break;
|
|
case 'md':
|
|
case 'markdown':
|
|
return 'text/gemini';
|
|
break;
|
|
case 'ans':
|
|
case 'ansi':
|
|
return 'text/x-ansi';
|
|
break;
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make a directory index suitable as response content
|
|
*
|
|
* @param string $path Current path
|
|
* @param string $root Root path on disk of the server
|
|
* @return string
|
|
*/
|
|
public function makeDirectoryIndex($path, $root): string
|
|
{
|
|
$files = glob($path . "*");
|
|
|
|
$body = "# Directory listing " . str_replace($root, '', $path) . "/\n\n";
|
|
|
|
if ($path != $root . "/") {
|
|
// If not already at root, provide option to go up one parent
|
|
$body .= "=> " . str_replace($root, '', dirname($path)) . "/ ..\n";
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
$relative_path = str_replace($path, '', $file);
|
|
|
|
$is_dir = false;
|
|
if (is_dir($file)) {
|
|
$is_dir = true;
|
|
$size = '';
|
|
} else {
|
|
$size = filesize($file);
|
|
}
|
|
|
|
$body .= sprintf(
|
|
"=> %s%s %s%s%s\n",
|
|
urlencode($relative_path),
|
|
($is_dir ? '/' : ''),
|
|
$relative_path,
|
|
($is_dir ? '/' : ''),
|
|
($size ? " ($size)" : '')
|
|
);
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Report whether a given file is world readable or not
|
|
*
|
|
* @param string $file The file to check
|
|
* @return bool
|
|
*/
|
|
public static function isWorldReadable(string $file): bool
|
|
{
|
|
return (bool)(fileperms($file) & self::WORLD_READABLE);
|
|
}
|
|
}
|