Merge branch 'master' into feat/route-compact-syntax

pull/596/head
n0nag0n 4 months ago committed by GitHub
commit bc001b7104
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,23 @@
name: Pull Request Check
on: [pull_request]
jobs:
unit-test:
name: Unit testing
strategy:
fail-fast: false
matrix:
php: [7.4, 8.0, 8.1, 8.2, 8.3]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, mbstring
tools: composer:v2
- run: composer install
- run: composer test

@ -41,7 +41,7 @@
},
"require-dev": {
"ext-pdo_sqlite": "*",
"flightphp/runway": "^0.2.0",
"flightphp/runway": "^0.2.3 || ^1.0",
"league/container": "^4.2",
"level-2/dice": "^4.0",
"phpstan/extension-installer": "^1.3",

@ -64,9 +64,10 @@ use flight\net\Route;
* @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* Sends a JSONP response.
*
* # HTTP caching
* # HTTP methods
* @method void etag(string $id, ('strong'|'weak') $type = 'strong') Handles ETag HTTP caching.
* @method void lastModified(int $time) Handles last modified HTTP caching.
* @method void download(string $filePath) Downloads a file
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
*/
@ -78,7 +79,7 @@ class Engine
private const MAPPABLE_METHODS = [
'start', 'stop', 'route', 'halt', 'error', 'notFound',
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp',
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'resource'
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource'
];
/** @var array<string, mixed> Stored variables. */
@ -600,7 +601,10 @@ class Engine
public function _error(Throwable $e): void
{
$msg = sprintf(
<<<HTML
HTML
<h1>500 Internal Server Error</h1>
<h3>%s (%s)</h3>
<pre>%s</pre>
@ -612,6 +616,7 @@ class Engine
try {
$this->response()
->cache(0)
->clearBody()
->status(500)
->write($msg)
@ -752,6 +757,10 @@ class Engine
*/
public function _halt(int $code = 200, string $message = '', bool $actuallyExit = true): void
{
if ($this->response()->getHeader('Cache-Control') === null) {
$this->response()->cache(0);
}
$this->response()
->clearBody()
->status($code)
@ -906,6 +915,20 @@ class Engine
}
}
/**
* Downloads a file
*
* @param string $filePath The path to the file to download
*
* @throws Exception If the file cannot be found
*
* @return void
*/
public function _download(string $filePath): void
{
$this->response()->downloadFile($filePath);
}
/**
* Handles ETag HTTP caching.
*

@ -70,16 +70,17 @@ require_once __DIR__ . '/autoload.php';
* @method static void redirect(string $url, int $code = 303) Redirects to another URL.
* @method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* Sends a JSON response.
* @method void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* @method static void jsonHalt(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0)
* Sends a JSON response and immediately halts the request.
* @method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512)
* Sends a JSONP response.
* @method static void error(Throwable $exception) Sends an HTTP 500 response.
* @method static void notFound() Sends an HTTP 404 response.
*
* # HTTP caching
* # HTTP methods
* @method static void etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching.
* @method static void lastModified(int $time) Performs last modified HTTP caching.
* @method static void download(string $filePath) Downloads a file
*/
class Flight
{

@ -414,4 +414,63 @@ class Request
return 'http';
}
/**
* Retrieves the array of uploaded files.
*
* @return array<string, array<string,UploadedFile>|array<string,array<string,UploadedFile>>> The array of uploaded files.
*/
public function getUploadedFiles(): array
{
$files = [];
$correctedFilesArray = $this->reArrayFiles($this->files);
foreach ($correctedFilesArray as $keyName => $files) {
foreach ($files as $file) {
$UploadedFile = new UploadedFile(
$file['name'],
$file['type'],
$file['size'],
$file['tmp_name'],
$file['error']
);
if (count($files) > 1) {
$files[$keyName][] = $UploadedFile;
} else {
$files[$keyName] = $UploadedFile;
}
}
}
return $files;
}
/**
* Re-arranges the files in the given files collection.
*
* @param Collection $filesCollection The collection of files to be re-arranged.
*
* @return array<string, array<int, array<string, mixed>>> The re-arranged files collection.
*/
protected function reArrayFiles(Collection $filesCollection): array
{
$fileArray = [];
foreach ($filesCollection as $fileKeyName => $file) {
$isMulti = is_array($file['name']) === true && count($file['name']) > 1;
$fileCount = $isMulti === true ? count($file['name']) : 1;
$fileKeys = array_keys($file);
for ($i = 0; $i < $fileCount; $i++) {
foreach ($fileKeys as $key) {
if ($isMulti === true) {
$fileArray[$fileKeyName][$i][$key] = $file[$key][$i];
} else {
$fileArray[$fileKeyName][$i][$key] = $file[$key];
}
}
}
}
return $fileArray;
}
}

@ -286,15 +286,9 @@ class Response
*/
public function cache($expires): self
{
if ($expires === false) {
if ($expires === false || $expires === 0) {
$this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
$this->headers['Cache-Control'] = [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
];
$this->headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0';
$this->headers['Pragma'] = 'no-cache';
} else {
$expires = \is_int($expires) ? $expires : strtotime($expires);
@ -437,8 +431,13 @@ class Response
$this->processResponseCallbacks();
}
if (headers_sent() === false) {
$this->sendHeaders(); // @codeCoverageIgnore
if ($this->headersSent() === false) {
// If you haven't set a Cache-Control header, we'll assume you don't want caching
if ($this->getHeader('Cache-Control') === null) {
$this->cache(false);
}
$this->sendHeaders();
}
echo $this->body;
@ -446,6 +445,17 @@ class Response
$this->sent = true;
}
/**
* Headers have been sent
*
* @return bool
* @codeCoverageIgnore
*/
public function headersSent(): bool
{
return headers_sent();
}
/**
* Adds a callback to process the response body before it's sent. These are processed in the order
* they are added
@ -470,4 +480,42 @@ class Response
$this->body = $callback($this->body);
}
}
/**
* Downloads a file.
*
* @param string $filePath The path to the file to be downloaded.
*
* @return void
*/
public function downloadFile(string $filePath): void
{
if (file_exists($filePath) === false) {
throw new Exception("$filePath cannot be found.");
}
$fileSize = filesize($filePath);
$mimeType = mime_content_type($filePath);
$mimeType = $mimeType !== false ? $mimeType : 'application/octet-stream';
$this->send();
$this->setRealHeader('Content-Description: File Transfer');
$this->setRealHeader('Content-Type: ' . $mimeType);
$this->setRealHeader('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
$this->setRealHeader('Expires: 0');
$this->setRealHeader('Cache-Control: must-revalidate');
$this->setRealHeader('Pragma: public');
$this->setRealHeader('Content-Length: ' . $fileSize);
// // Clear the output buffer
ob_clean();
flush();
// // Read the file and send it to the output buffer
readfile($filePath);
if (empty(getenv('PHPUNIT_TEST'))) {
exit; // @codeCoverageIgnore
}
}
}

@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace flight\net;
use Exception;
class UploadedFile
{
/**
* @var string $name The name of the uploaded file.
*/
private string $name;
/**
* @var string $mimeType The MIME type of the uploaded file.
*/
private string $mimeType;
/**
* @var int $size The size of the uploaded file in bytes.
*/
private int $size;
/**
* @var string $tmpName The temporary name of the uploaded file.
*/
private string $tmpName;
/**
* @var int $error The error code associated with the uploaded file.
*/
private int $error;
/**
* Constructs a new UploadedFile object.
*
* @param string $name The name of the uploaded file.
* @param string $mimeType The MIME type of the uploaded file.
* @param int $size The size of the uploaded file in bytes.
* @param string $tmpName The temporary name of the uploaded file.
* @param int $error The error code associated with the uploaded file.
*/
public function __construct(string $name, string $mimeType, int $size, string $tmpName, int $error)
{
$this->name = $name;
$this->mimeType = $mimeType;
$this->size = $size;
$this->tmpName = $tmpName;
$this->error = $error;
}
/**
* Retrieves the client-side filename of the uploaded file.
*
* @return string The client-side filename.
*/
public function getClientFilename(): string
{
return $this->name;
}
/**
* Retrieves the media type of the uploaded file as provided by the client.
*
* @return string The media type of the uploaded file.
*/
public function getClientMediaType(): string
{
return $this->mimeType;
}
/**
* Returns the size of the uploaded file.
*
* @return int The size of the uploaded file.
*/
public function getSize(): int
{
return $this->size;
}
/**
* Retrieves the temporary name of the uploaded file.
*
* @return string The temporary name of the uploaded file.
*/
public function getTempName(): string
{
return $this->tmpName;
}
/**
* Get the error code associated with the uploaded file.
*
* @return int The error code.
*/
public function getError(): int
{
return $this->error;
}
/**
* Moves the uploaded file to the specified target path.
*
* @param string $targetPath The path to move the file to.
*
* @return void
*/
public function moveTo(string $targetPath): void
{
if ($this->error !== UPLOAD_ERR_OK) {
throw new Exception($this->getUploadErrorMessage($this->error));
}
$isUploadedFile = is_uploaded_file($this->tmpName) === true;
if (
$isUploadedFile === true
&&
move_uploaded_file($this->tmpName, $targetPath) === false
) {
throw new Exception('Cannot move uploaded file'); // @codeCoverageIgnore
} elseif ($isUploadedFile === false && getenv('PHPUNIT_TEST')) {
rename($this->tmpName, $targetPath);
}
}
/**
* Retrieves the error message for a given upload error code.
*
* @param int $error The upload error code.
*
* @return string The error message.
*/
protected function getUploadErrorMessage(int $error): string
{
switch ($error) {
case UPLOAD_ERR_INI_SIZE:
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
case UPLOAD_ERR_FORM_SIZE:
return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
case UPLOAD_ERR_PARTIAL:
return 'The uploaded file was only partially uploaded.';
case UPLOAD_ERR_NO_FILE:
return 'No file was uploaded.';
case UPLOAD_ERR_NO_TMP_DIR:
return 'Missing a temporary folder.';
case UPLOAD_ERR_CANT_WRITE:
return 'Failed to write file to disk.';
case UPLOAD_ERR_EXTENSION:
return 'A PHP extension stopped the file upload.';
default:
return 'An unknown error occurred. Error code: ' . $error;
}
}
}

@ -813,12 +813,15 @@ class EngineTest extends TestCase
$engine->request()->url = '/container';
// php 7.4 will throw a PDO exception, but php 8 will throw an ErrorException
if(version_compare(PHP_VERSION, '8.0.0', '<')) {
$this->expectException(PDOException::class);
$this->expectExceptionMessageMatches("/invalid data source name/");
} else {
if(version_compare(PHP_VERSION, '8.1.0') >= 0) {
$this->expectException(ErrorException::class);
$this->expectExceptionMessageMatches("/Passing null to parameter/");
} elseif(version_compare(PHP_VERSION, '8.0.0') >= 0) {
$this->expectException(PDOException::class);
$this->expectExceptionMessageMatches("/must be a valid data source name/");
} else {
$this->expectException(PDOException::class);
$this->expectExceptionMessageMatches("/invalid data source name/");
}
$engine->start();
@ -952,4 +955,38 @@ class EngineTest extends TestCase
$this->assertEquals('Method Not Allowed', $engine->response()->getBody());
}
public function testDownload()
{
$engine = new class extends Engine {
public function getLoader()
{
return $this->loader;
}
};
// doing this so we can overwrite some parts of the response
$engine->getLoader()->register('response', function () {
return new class extends Response {
public function setRealHeader(
string $header_string,
bool $replace = true,
int $response_code = 0
): self {
return $this;
}
};
});
$tmpfile = tmpfile();
fwrite($tmpfile, 'I am a teapot');
$streamPath = stream_get_meta_data($tmpfile)['uri'];
$this->expectOutputString('I am a teapot');
$engine->download($streamPath);
}
public function testDownloadBadPath() {
$engine = new Engine();
$this->expectException(Exception::class);
$this->expectExceptionMessage("/path/to/nowhere cannot be found.");
$engine->download('/path/to/nowhere');
}
}

@ -41,23 +41,23 @@ class RequestTest extends TestCase
public function testDefaults()
{
self::assertEquals('/', $this->request->url);
self::assertEquals('/', $this->request->base);
self::assertEquals('GET', $this->request->method);
self::assertEquals('', $this->request->referrer);
self::assertTrue($this->request->ajax);
self::assertEquals('http', $this->request->scheme);
self::assertEquals('', $this->request->type);
self::assertEquals(0, $this->request->length);
self::assertFalse($this->request->secure);
self::assertEquals('', $this->request->accept);
self::assertEquals('example.com', $this->request->host);
$this->assertEquals('/', $this->request->url);
$this->assertEquals('/', $this->request->base);
$this->assertEquals('GET', $this->request->method);
$this->assertEquals('', $this->request->referrer);
$this->assertTrue($this->request->ajax);
$this->assertEquals('http', $this->request->scheme);
$this->assertEquals('', $this->request->type);
$this->assertEquals(0, $this->request->length);
$this->assertFalse($this->request->secure);
$this->assertEquals('', $this->request->accept);
$this->assertEquals('example.com', $this->request->host);
}
public function testIpAddress()
{
self::assertEquals('8.8.8.8', $this->request->ip);
self::assertEquals('32.32.32.32', $this->request->proxy_ip);
$this->assertEquals('8.8.8.8', $this->request->ip);
$this->assertEquals('32.32.32.32', $this->request->proxy_ip);
}
public function testSubdirectory()
@ -66,7 +66,7 @@ class RequestTest extends TestCase
$request = new Request();
self::assertEquals('/subdir', $request->base);
$this->assertEquals('/subdir', $request->base);
}
public function testQueryParameters()
@ -75,9 +75,9 @@ class RequestTest extends TestCase
$request = new Request();
self::assertEquals('/page?id=1&name=bob', $request->url);
self::assertEquals(1, $request->query->id);
self::assertEquals('bob', $request->query->name);
$this->assertEquals('/page?id=1&name=bob', $request->url);
$this->assertEquals(1, $request->query->id);
$this->assertEquals('bob', $request->query->name);
}
public function testCollections()
@ -91,11 +91,11 @@ class RequestTest extends TestCase
$request = new Request();
self::assertEquals(1, $request->query->q);
self::assertEquals(1, $request->query->id);
self::assertEquals(1, $request->data->q);
self::assertEquals(1, $request->cookies->q);
self::assertEquals(1, $request->files->q);
$this->assertEquals(1, $request->query->q);
$this->assertEquals(1, $request->query->id);
$this->assertEquals(1, $request->data->q);
$this->assertEquals(1, $request->cookies->q);
$this->assertEquals(1, $request->files->q);
}
public function testJsonWithEmptyBody()
@ -104,7 +104,7 @@ class RequestTest extends TestCase
$request = new Request();
self::assertSame([], $request->data->getData());
$this->assertSame([], $request->data->getData());
}
public function testMethodOverrideWithHeader()
@ -113,7 +113,7 @@ class RequestTest extends TestCase
$request = new Request();
self::assertEquals('PUT', $request->method);
$this->assertEquals('PUT', $request->method);
}
public function testMethodOverrideWithPost()
@ -122,38 +122,38 @@ class RequestTest extends TestCase
$request = new Request();
self::assertEquals('PUT', $request->method);
$this->assertEquals('PUT', $request->method);
}
public function testHttps()
{
$_SERVER['HTTPS'] = 'on';
$request = new Request();
self::assertEquals('https', $request->scheme);
$this->assertEquals('https', $request->scheme);
$_SERVER['HTTPS'] = 'off';
$request = new Request();
self::assertEquals('http', $request->scheme);
$this->assertEquals('http', $request->scheme);
$_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https';
$request = new Request();
self::assertEquals('https', $request->scheme);
$this->assertEquals('https', $request->scheme);
$_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http';
$request = new Request();
self::assertEquals('http', $request->scheme);
$this->assertEquals('http', $request->scheme);
$_SERVER['HTTP_FRONT_END_HTTPS'] = 'on';
$request = new Request();
self::assertEquals('https', $request->scheme);
$this->assertEquals('https', $request->scheme);
$_SERVER['HTTP_FRONT_END_HTTPS'] = 'off';
$request = new Request();
self::assertEquals('http', $request->scheme);
$this->assertEquals('http', $request->scheme);
$_SERVER['REQUEST_SCHEME'] = 'https';
$request = new Request();
self::assertEquals('https', $request->scheme);
$this->assertEquals('https', $request->scheme);
$_SERVER['REQUEST_SCHEME'] = 'http';
$request = new Request();
self::assertEquals('http', $request->scheme);
$this->assertEquals('http', $request->scheme);
}
public function testInitUrlSameAsBaseDirectory()
@ -279,4 +279,54 @@ class RequestTest extends TestCase
$request = new Request();
$this->assertEquals('https://localhost:8000', $request->getBaseUrl());
}
public function testGetSingleFileUpload()
{
$_FILES['file'] = [
'name' => 'file.txt',
'type' => 'text/plain',
'size' => 123,
'tmp_name' => '/tmp/php123',
'error' => 0
];
$request = new Request();
$file = $request->getUploadedFiles()['file'];
$this->assertEquals('file.txt', $file->getClientFilename());
$this->assertEquals('text/plain', $file->getClientMediaType());
$this->assertEquals(123, $file->getSize());
$this->assertEquals('/tmp/php123', $file->getTempName());
$this->assertEquals(0, $file->getError());
}
public function testGetMultiFileUpload()
{
$_FILES['files'] = [
'name' => ['file1.txt', 'file2.txt'],
'type' => ['text/plain', 'text/plain'],
'size' => [123, 456],
'tmp_name' => ['/tmp/php123', '/tmp/php456'],
'error' => [0, 0]
];
$request = new Request();
$files = $request->getUploadedFiles()['files'];
$this->assertCount(2, $files);
$this->assertEquals('file1.txt', $files[0]->getClientFilename());
$this->assertEquals('text/plain', $files[0]->getClientMediaType());
$this->assertEquals(123, $files[0]->getSize());
$this->assertEquals('/tmp/php123', $files[0]->getTempName());
$this->assertEquals(0, $files[0]->getError());
$this->assertEquals('file2.txt', $files[1]->getClientFilename());
$this->assertEquals('text/plain', $files[1]->getClientMediaType());
$this->assertEquals(456, $files[1]->getSize());
$this->assertEquals('/tmp/php456', $files[1]->getTempName());
$this->assertEquals(0, $files[1]->getError());
}
}

@ -164,11 +164,7 @@ class ResponseTest extends TestCase
$response->cache(false);
$this->assertEquals([
'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
'Cache-Control' => [
'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0',
'max-age=0',
],
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache'
], $response->headers());
}
@ -239,6 +235,46 @@ class ResponseTest extends TestCase
$this->assertTrue($response->sent());
}
public function testSendWithNoHeadersSent()
{
$response = new class extends Response {
protected $test_sent_headers = [];
public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self
{
$this->test_sent_headers[] = $header_string;
return $this;
}
public function getSentHeaders(): array
{
return $this->test_sent_headers;
}
public function headersSent(): bool
{
return false;
}
};
$response->header('Content-Type', 'text/html');
$response->header('X-Test', 'test');
$response->write('Something');
$this->expectOutputString('Something');
$response->send();
$sent_headers = $response->getSentHeaders();
$this->assertEquals([
'HTTP/1.1 200 OK',
'Content-Type: text/html',
'X-Test: test',
'Expires: Mon, 26 Jul 1997 05:00:00 GMT',
'Cache-Control: no-store, no-cache, must-revalidate, max-age=0',
'Pragma: no-cache',
'Content-Length: 9'
], $sent_headers);
}
public function testClearBody()
{
$response = new Response();
@ -282,7 +318,16 @@ class ResponseTest extends TestCase
ob_start();
$response->send();
$gzip_body = ob_get_clean();
$expected = PHP_OS === 'WINNT' ? 'H4sIAAAAAAAACitJLS4BAAx+f9gEAAAA' : 'H4sIAAAAAAAAAytJLS4BAAx+f9gEAAAA';
switch (PHP_OS) {
case 'WINNT':
$expected = 'H4sIAAAAAAAACitJLS4BAAx+f9gEAAAA';
break;
case 'Darwin':
$expected = 'H4sIAAAAAAAAEytJLS4BAAx+f9gEAAAA';
break;
default:
$expected = 'H4sIAAAAAAAAAytJLS4BAAx+f9gEAAAA';
}
$this->assertEquals($expected, base64_encode($gzip_body));
$this->assertEquals(strlen(gzencode('test')), strlen($gzip_body));
}

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace tests;
use Exception;
use flight\net\UploadedFile;
use PHPUnit\Framework\TestCase;
class UploadedFileTest extends TestCase
{
public function tearDown(): void
{
if (file_exists('file.txt')) {
unlink('file.txt');
}
if (file_exists('tmp_name')) {
unlink('tmp_name');
}
}
public function testMoveToSuccess()
{
file_put_contents('tmp_name', 'test');
$uploadedFile = new UploadedFile('file.txt', 'text/plain', 4, 'tmp_name', UPLOAD_ERR_OK);
$uploadedFile->moveTo('file.txt');
$this->assertFileExists('file.txt');
}
public function getFileErrorMessageTests(): array
{
return [
[ UPLOAD_ERR_INI_SIZE, 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', ],
[ UPLOAD_ERR_FORM_SIZE, 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', ],
[ UPLOAD_ERR_PARTIAL, 'The uploaded file was only partially uploaded.', ],
[ UPLOAD_ERR_NO_FILE, 'No file was uploaded.', ],
[ UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder.', ],
[ UPLOAD_ERR_CANT_WRITE, 'Failed to write file to disk.', ],
[ UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the file upload.', ],
[ -1, 'An unknown error occurred. Error code: -1' ]
];
}
/**
* @dataProvider getFileErrorMessageTests
*/
public function testMoveToFailureMessages($error, $message)
{
file_put_contents('tmp_name', 'test');
$uploadedFile = new UploadedFile('file.txt', 'text/plain', 4, 'tmp_name', $error);
$this->expectException(Exception::class);
$this->expectExceptionMessage($message);
$uploadedFile->moveTo('file.txt');
}
}

@ -86,6 +86,7 @@ class LayoutMiddleware
<li><a href="/dice">Dice Container</a></li>
<li><a href="/no-container">No Container Registered</a></li>
<li><a href="/Pascal_Snake_Case">Pascal_Snake_Case</a></li>
<li><a href="/download">Download File</a></li>
</ul>
HTML;
echo '<div id="container">';

@ -175,6 +175,11 @@ Flight::route('/json-halt', function () {
Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']);
});
// Download a file
Flight::route('/download', function () {
Flight::download('test_file.txt');
});
Flight::map('error', function (Throwable $e) {
echo sprintf(
<<<HTML

@ -0,0 +1 @@
This file downloaded successfully!
Loading…
Cancel
Save