orbit/src/Orbit/Module/Statics.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);
}
}