-
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add BinaryContentHandler to handle application/octet-stream
- Loading branch information
1 parent
39620b9
commit 09fd1c9
Showing
2 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
92 changes: 92 additions & 0 deletions
92
pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/ContentHandlers/BinaryContentHandler.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
<?php | ||
|
||
namespace RESTAPI\ContentHandlers; | ||
|
||
use RESTAPI\Core\ContentHandler; | ||
use RESTAPI\Core\Response; | ||
use RESTAPI\Responses\NotAcceptableError; | ||
use RESTAPI\Responses\ServerError; | ||
use RESTAPI\Responses\Success; | ||
|
||
/** | ||
* Defines a ContentHandler for handling binary data. This is primarily used to handle file downloads. | ||
* @note In context to Models, this content handler can only encode the data as a binary file for download if | ||
* the model contains both a $filename containing the name of the file to download and a $binary_data containing | ||
* the binary data to download. | ||
*/ | ||
class BinaryContentHandler extends ContentHandler { | ||
/** | ||
* The MIME type associated with this content handler. | ||
* @var string $mime_type | ||
*/ | ||
public string $mime_type = 'application/octet-stream'; | ||
|
||
/** | ||
* The help text for encoding content with this content handler. | ||
* @var string $encode_help_text | ||
*/ | ||
public string $encode_help_text = 'Downloads the response as generic binary data.'; | ||
|
||
/** | ||
* Encodes/preps the given content as a binary file for download. | ||
* @param mixed|null $content The content to encode. | ||
* @param Response|null $context The Response object that contains the content to encode. | ||
* @return mixed The encoded content. | ||
*/ | ||
protected function _encode(mixed $content = null, ?Response $context = null): mixed { | ||
# We cannot encode error messages. If an error, rethrow it to be handled by the default Response handler | ||
if (!$context instanceof Success) { | ||
throw $context; | ||
} | ||
|
||
# Extract the content's data array | ||
$content = $content['data']; | ||
|
||
# Ensure te content provided can even be encoded as a binary file | ||
if (!is_array($content) or !$content['binary_data'] or !$content['filename']) { | ||
throw new NotAcceptableError( | ||
message: 'The requested resource cannot be encoded as a binary file.', | ||
response_id: 'BINARY_CONTENT_HANDLER_RESOURCE_NOT_SUPPORTED', | ||
); | ||
} | ||
|
||
# Ensure the filename is actually a filename | ||
if (!preg_match('/^[a-zA-Z0-9_\-.]+$/', $content['filename'])) { | ||
throw new ServerError( | ||
message: "The requested filename for this resource is invalid.", | ||
response_id: 'BINARY_CONTENT_HANDLER_INVALID_FILENAME', | ||
); | ||
} | ||
|
||
# Ensure the data is actually binary data | ||
if (mb_detect_encoding($content['binary_data'], 'UTF-8', true) !== false) { | ||
throw new ServerError( | ||
message: "The requested data does not appear to be binary data.", | ||
response_id: 'BINARY_CONTENT_HANDLER_INVALID_DATA', | ||
); | ||
} | ||
|
||
header('Content-Description: File Transfer'); | ||
header('Content-Disposition: attachment; filename="' . $content['filename'] . '"'); | ||
header('Cache-Control: must-revalidate'); | ||
header('Pragma: public'); | ||
header('Expires: 0'); | ||
return $content['binary_data']; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function to_openapi_schema(Response $response): array | ||
{ | ||
return [ | ||
$this->mime_type => [ | ||
'description' => $this->encode_help_text, | ||
'schema' => [ | ||
'type' => 'string', | ||
'format' => 'binary', | ||
], | ||
], | ||
]; | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
...TAPI/files/usr/local/pkg/RESTAPI/Tests/APIContentHandlersBinaryContentHandlerTestCase.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
namespace RESTAPI\Tests; | ||
|
||
use RESTAPI\ContentHandlers\BinaryContentHandler; | ||
use RESTAPI\Core\TestCase; | ||
use RESTAPI\Responses\Success; | ||
|
||
class APIContentHandlersBinaryContentHandlerTestCase extends TestCase | ||
{ | ||
/** | ||
* Ensure the encode() method throws an error if the required 'binary_data' and 'filename' keys are not present in | ||
* the content array. | ||
*/ | ||
public function test_encode_requires_binary_data_and_filename_keys(): void | ||
{ | ||
$this->assert_throws_response( | ||
response_id: 'BINARY_CONTENT_HANDLER_RESOURCE_NOT_SUPPORTED', | ||
code: 406, | ||
callable: function () { | ||
$handler = new BinaryContentHandler(); | ||
$response = new Success(message: '', response_id: ''); # Required as context for the encode() method | ||
$handler->encode(['data' => []], $response); | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Ensure the encode() method throws an error if the 'filename' key contains invalid characters. | ||
*/ | ||
public function test_encode_requires_valid_filename(): void | ||
{ | ||
$this->assert_throws_response( | ||
response_id: 'BINARY_CONTENT_HANDLER_INVALID_FILENAME', | ||
code: 500, | ||
callable: function () { | ||
$handler = new BinaryContentHandler(); | ||
$response = new Success(message: '', response_id: ''); # Required as context for the encode() method | ||
$handler->encode(['data' => ['binary_data' => 'data', 'filename' => 'invalid file name']], $response); | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Ensure the encode() method does not throw an error if the required 'binary_data' and 'filename' keys are present in | ||
* the content array. | ||
*/ | ||
public function test_encode_requires_binary_data(): void { | ||
$this->assert_throws_response( | ||
response_id: 'BINARY_CONTENT_HANDLER_INVALID_DATA', | ||
code: 500, | ||
callable: function () { | ||
$handler = new BinaryContentHandler(); | ||
$response = new Success(message: '', response_id: ''); # Required as context for the encode() method | ||
$handler->encode( | ||
['data' => ['binary_data' => 'data', 'filename' => 'valid_file_name.txt']], | ||
$response | ||
); | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Ensures the encode() method correctly encodes the content as a binary file for download. | ||
*/ | ||
public function test_encode(): void | ||
{ | ||
$this->assert_does_not_throw( | ||
callable: function () { | ||
$response = new Success(message: '', response_id: ''); | ||
$handler = new BinaryContentHandler(); | ||
$binary_data = random_bytes(16); | ||
$handler->encode( | ||
['data' => ['binary_data' => $binary_data, 'filename' => 'valid_file_name.txt']], | ||
$response | ||
); | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Ensures the to_openapi_schema() method returns the correct schema for the BinaryContentHandler. | ||
*/ | ||
public function test_to_openapi_schema(): void | ||
{ | ||
$handler = new BinaryContentHandler(); | ||
$response = new Success(message: '', response_id: ''); # Required as context for the to_openapi_schema() method | ||
$this->assert_equals( | ||
$handler->to_openapi_schema($response), | ||
[ | ||
$handler->mime_type => [ | ||
'description' => $handler->encode_help_text, | ||
'schema' => [ | ||
'type' => 'string', | ||
'format' => 'binary', | ||
], | ||
], | ||
] | ||
); | ||
} | ||
|
||
} |