abbadon1334/atk4-fastroute

View on GitHub
src/Handler/RoutedServeStatic.php

Summary

Maintainability
A
0 mins
Test Coverage
B
88%
<?php

declare(strict_types=1);

namespace Abbadon1334\ATKFastRoute\Handler;

use Abbadon1334\ATKFastRoute\Exception\StaticFileExtensionNotAllowed;
use Abbadon1334\ATKFastRoute\Exception\StaticFileNotExists;
use Abbadon1334\ATKFastRoute\Handler\Contracts\AfterRoutableTrait;
use Abbadon1334\ATKFastRoute\Handler\Contracts\BeforeRoutableTrait;
use Abbadon1334\ATKFastRoute\Handler\Contracts\iAfterRoutable;
use Abbadon1334\ATKFastRoute\Handler\Contracts\iArrayable;
use Abbadon1334\ATKFastRoute\Handler\Contracts\iBeforeRoutable;
use Abbadon1334\ATKFastRoute\Handler\Contracts\iOnRoute;
use Mimey\MimeTypes;
use Throwable;

class RoutedServeStatic implements iOnRoute, iArrayable, iAfterRoutable, iBeforeRoutable
{
    use AfterRoutableTrait {
        OnAfterRoute as _OnAfterRoute;
    }

    use BeforeRoutableTrait {
        OnBeforeRoute as _OnBeforeRoute;
    }

    /** @var string */
    protected $path;

    /** @var array */
    protected $extensions = [];

    /**
     * RoutedCallable constructor.
     *
     * @param string $path Base path for serving static files
     */
    public function __construct(string $path, array $extensions)
    {
        $this->path = $path;
        $this->extensions = $extensions;
    }

    public function toArray(): array
    {
        return [$this->path, $this->extensions];
    }

    public function onRoute(...$parameters): void
    {
        $request_path = array_shift($parameters);

        // remove query part;
        $request_path = strtok($request_path, '?');

        // get path parts
        $path = pathinfo($request_path, PATHINFO_DIRNAME);
        $file = pathinfo($request_path, PATHINFO_BASENAME);

        $folder_path = $this->getFolderPath($path);

        try {
            $this->assertDirIsAllowed($folder_path);

            $file_path = $folder_path . \DIRECTORY_SEPARATOR . $file;
            $this->assertFileIsAllowed($file_path);

            $this->serveFile($file_path);
        } catch (Throwable $t) {
            http_response_code(403);
            echo $t->getMessage();
        }
    }

    private function getFolderPath(string $path = null): string
    {
        return $path === null || $path === '.'
            ? $this->path
            : implode(\DIRECTORY_SEPARATOR, [$this->path, $path]);
    }

    private function assertDirIsAllowed(string $path): void
    {
        $path = realpath($path);
        $vroot = getcwd();

        if ($path === false || substr($path, 0, strlen($vroot)) !== $vroot || !is_dir($path)) {
            throw (new StaticFileExtensionNotAllowed('Requested file folder is not allowed'))
                ->addMoreInfo('path', $path)
                ->addMoreInfo('fullpath', realpath($path));
        }
    }

    private function assertFileIsAllowed(string $filepath): void
    {
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);

        if (!$this->isExtensionAllowed($ext)) {
            throw (new StaticFileExtensionNotAllowed('Extension is not allowed'))
                ->addMoreInfo('ext', $ext);
        }

        if (!file_exists($filepath)) {
            throw new StaticFileNotExists('Requested File extension not exists');
        }
    }

    private function isExtensionAllowed(string $ext): bool
    {
        return in_array($ext, $this->extensions, true);
    }

    private function serveFile(string $file_path): void
    {
        http_response_code(200);

        $filename = pathinfo($file_path, PATHINFO_BASENAME);
        $ext = pathinfo($file_path, PATHINFO_EXTENSION);

        $mimeType = (new MimeTypes())->getMimeType($ext);

        header('Cache-Control: max-age=86400');
        header('X-Sendfile: ' . $file_path);
        // header("Content-Type: application/octet-stream");
        header('Content-Type: ' . $mimeType . '');
        header('Content-Disposition: attachment; filename="' . $filename . '"');

        readfile($file_path);
    }
}