Skip to content

Commit

Permalink
feat: add BinaryContentHandler to handle application/octet-stream
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredhendrickson13 committed Sep 9, 2024
1 parent 39620b9 commit 09fd1c9
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
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',
],
],
];
}
}
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',
],
],
]
);
}

}

0 comments on commit 09fd1c9

Please sign in to comment.