orbit/src/Orbit/Module/Statics.php

198 lines
5.9 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
// getAbsolutePath will translate any '..' in the path
$realpath = self::getAbsolutePath($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);
}
/**
* Get an absolute path for a filename
*
* Translates .. and . to the real locations. The reason I am not using
* realpath() to do it is it resolves symlinks
*
* @param string $path
* @return string
*/
public static function getAbsolutePath($path): string
{
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutes = [];
foreach ($parts as $part) {
if ('.' == $part) {
continue;
}
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
return "/" . implode(DIRECTORY_SEPARATOR, $absolutes);
}
}