Skip to content

Commit

Permalink
feat(core): added download response
Browse files Browse the repository at this point in the history
ref #11178
  • Loading branch information
jeabakker committed Nov 4, 2022
1 parent 6fea5ba commit 12204cc
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 61 deletions.
11 changes: 3 additions & 8 deletions actions/diagnostics/download.php
Expand Up @@ -9,11 +9,6 @@
$output = elgg_echo('diagnostics:header', [date('r'), elgg_get_logged_in_user_entity()->getDisplayName()]);
$output = elgg_trigger_event_results('diagnostics:report', 'system', [], $output);

header("Cache-Control: public");
header("Content-Description: File Transfer");
header('Content-disposition: attachment; filename=elggdiagnostic.txt');
header("Content-Type: text/plain");
header('Content-Length: ' . strlen($output));

echo $output;
exit();
return elgg_download_response($output, 'elggdiagnostic.txt', false, [
'Content-Type' => 'text/plain; charset=utf-8',
]);
69 changes: 69 additions & 0 deletions engine/classes/Elgg/Http/DownloadResponse.php
@@ -0,0 +1,69 @@
<?php

namespace Elgg\Http;

/**
* Download response builder
*
* @since 5.0
*/
class DownloadResponse extends OkResponse {

/**
* {@inheritDoc}
*/
public function getHeaders() {
$headers = parent::getHeaders();

if (!isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/octet-stream; charset=utf-8';
}

if (!isset($headers['Cache-Control'])) {
$headers['Cache-Control'] = 'no-store';
}

if (!isset($headers['Content-Disposition'])) {
$headers['Content-Disposition'] = 'attachment';
}

if (!empty($this->content) && !isset($headers['Content-Length'])) {
$headers['Content-Length'] = strlen((string) $this->content);
}

return $headers;
}

/**
* {@inheritDoc}
*/
public function setForwardURL(string $forward_url = REFERRER) {
return $this;
}

/**
* Set the filename for the download
*
* This will only be applied if the 'Content-Disposition' header isn't already set
*
* @param string $filename The filename when downloaded
* @param bool $inline Is this an inline download (default: false, determines the 'Content-Disposition' header)
*
* @return self
*/
public function setFilename(string $filename = '', bool $inline = false): static {
if (isset($this->headers['Content-Disposition'])) {
return $this;
}

$disposition = $inline ? 'inline' : 'attachment';

if (!elgg_is_empty($filename)) {
$disposition .= "; filename=\"{$filename}\"";
}

$this->headers['Content-Disposition'] = $disposition;

return $this;
}
}
19 changes: 19 additions & 0 deletions engine/lib/pagehandler.php
Expand Up @@ -269,3 +269,22 @@ function elgg_error_response(string|array $message = '', string $forward_url = R
function elgg_redirect_response(string $forward_url = REFERRER, int $status_code = ELGG_HTTP_FOUND): \Elgg\Http\RedirectResponse {
return new Elgg\Http\RedirectResponse($forward_url, $status_code);
}

/**
* Prepare a download response
*
* @param string $content The content of the download
* @param string $filename The filename when downloaded
* @param bool $inline Is this an inline download (default: false, determines the 'Content-Disposition' header)
* @param array $headers (optional) additional headers for the response
*
* @return \Elgg\Http\DownloadResponse
* @since 5.0
*/
function elgg_download_response(string $content, string $filename = '', bool $inline = false, array $headers = []): \Elgg\Http\DownloadResponse {
$response = new \Elgg\Http\DownloadResponse($content);
$response->setHeaders($headers);
$response->setFilename($filename, $inline);

return $response;
}
@@ -0,0 +1,54 @@
<?php

namespace Elgg\Http;

use Elgg\IntegrationTestCase;

class DownloadResponseIntegrationTest extends IntegrationTestCase {

/**
* @dataProvider getDownloadResponseProvider
*/
public function testGetDownloadResponse(string $content, string $filename = '', bool $inline = false, array $headers = []) {
$response = elgg_download_response($content, $filename, $inline, $headers);

$this->assertInstanceOf(DownloadResponse::class, $response);
$this->assertEquals($content, $response->getContent());

$response_headers = $response->getHeaders();
$this->assertIsArray($response_headers);
$this->assertArrayHasKey('Content-Disposition', $response_headers);

if (!isset($headers['Content-Disposition'])) {
if (!empty($filename)) {
$this->assertStringContainsString("filename=\"{$filename}\"", $response_headers['Content-Disposition']);
} else {
$this->assertStringNotContainsString('filename', $response_headers['Content-Disposition']);
}

if ($inline) {
$this->assertStringContainsString('inline', $response_headers['Content-Disposition']);
} else {
$this->assertStringContainsString('attachment', $response_headers['Content-Disposition']);
}
}

foreach ($headers as $key => $value) {
$this->assertArrayHasKey($key, $response_headers);
$this->assertEquals($value, $response_headers[$key]);
}
}

public function getDownloadResponseProvider() {
return [
['foo'],
['foo', 'bar.txt'],
['foo', 'bar.txt', true],
['foo', 'bar.txt', false, ['Content-Type' => 'text/plain; charset=utf-8']],
['foo', 'bar.txt', false, [
'Content-Type' => 'text/json; charset=utf-8',
'Content-Disposition' => 'inline; filename="bar.json"',
]],
];
}
}
107 changes: 107 additions & 0 deletions engine/tests/phpunit/unit/Elgg/Http/DownloadResponseUnitTest.php
@@ -0,0 +1,107 @@
<?php

namespace Elgg\Http;

class DownloadResponseUnitTest extends ResponseUnitTestCase {

public function getReponseClassName(): string {
return DownloadResponse::class;
}

public function testCanConstructWihtoutArguments() {
$test_class = $this->getReponseClassName();
$response = new $test_class();

$this->assertEquals('', $response->getContent());
$this->assertEquals(ELGG_HTTP_OK, $response->getStatusCode());
$this->assertNull($response->getForwardURL());
$this->assertEquals([
'Content-Type' => 'application/octet-stream; charset=utf-8',
'Cache-Control' => 'no-store',
'Content-Disposition' => 'attachment',
], $response->getHeaders());
}

public function testCanConstructWithArguments() {
$content = 'foo';
$status_code = ELGG_HTTP_PARTIAL_CONTENT;

$test_class = $this->getReponseClassName();
$response = new $test_class($content, $status_code, REFERRER);

$this->assertEquals($content, $response->getContent());
$this->assertEquals($status_code, $response->getStatusCode());
$this->assertNull($response->getForwardURL());
$this->assertEquals([
'Content-Type' => 'application/octet-stream; charset=utf-8',
'Cache-Control' => 'no-store',
'Content-Disposition' => 'attachment',
'Content-Length' => strlen($content),
], $response->getHeaders());
}

public function testSetFilename() {
$test_class = $this->getReponseClassName();
$response = new $test_class();

// test attachment
$response->setFilename('foo');

$headers = $response->getHeaders();
$this->assertIsArray($headers);
$this->assertArrayHasKey('Content-Disposition', $headers);
$this->assertStringContainsString('filename="foo"', $headers['Content-Disposition']);
$this->assertStringContainsString('attachment', $headers['Content-Disposition']);

// test inline
$response->setHeaders([]); // need to clean headers
$response->setFilename('bar', true);

$headers = $response->getHeaders();
$this->assertIsArray($headers);
$this->assertArrayHasKey('Content-Disposition', $headers);
$this->assertStringContainsString('filename="bar"', $headers['Content-Disposition']);
$this->assertStringContainsString('inline', $headers['Content-Disposition']);

// test trying to overrule filename fails
$response->setFilename('notset');

$headers = $response->getHeaders();
$this->assertIsArray($headers);
$this->assertArrayHasKey('Content-Disposition', $headers);
$this->assertStringNotContainsString('filename="notset"', $headers['Content-Disposition']);
$this->assertStringNotContainsString('attachment', $headers['Content-Disposition']);
}

/**
* Overruled tests from parent because of changes to the DownloadResponse
*/

/**
* @dataProvider validForwardURLsProvider
*/
public function testCanSetForwardURL($value) {
$test_class = $this->getReponseClassName();
$response = new $test_class();

$response->setForwardURL($value);
$this->assertNull($response->getForwardURL());
}

public function testCanSetHeaders() {
$test_class = $this->getReponseClassName();
$response = new $test_class();
$this->assertEquals([
'Content-Type' => 'application/octet-stream; charset=utf-8',
'Cache-Control' => 'no-store',
'Content-Disposition' => 'attachment',
], $response->getHeaders());

$response->setHeaders(['Content-Type' => 'application/json']);
$this->assertEquals([
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store',
'Content-Disposition' => 'attachment',
], $response->getHeaders());
}
}
Expand Up @@ -6,15 +6,16 @@
* @group HttpService
* @group UnitTests
*/
class ErrorResponseUnitTest extends ResponseUnitTest {
class ErrorResponseUnitTest extends ResponseUnitTestCase {

public function up() {
$this->class = ErrorResponse::class;
public function getReponseClassName(): string {
return ErrorResponse::class;
}

public function testCanConstructWihtoutArguments() {
$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class();

$this->assertEquals('', $response->getContent());
$this->assertEquals(ELGG_HTTP_BAD_REQUEST, $response->getStatusCode());
$this->assertEquals(REFERRER, $response->getForwardURL());
Expand All @@ -25,8 +26,8 @@ public function testCanConstructWithArguments() {
$error = 'foo';
$status_code = ELGG_HTTP_NOT_FOUND;
$forward_url = REFERRER;

$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class($error, $status_code, $forward_url);

$this->assertEquals($error, $response->getContent());
Expand All @@ -36,8 +37,9 @@ public function testCanConstructWithArguments() {
}

public function testConstructWithInvalidStatusCode() {
$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class('foo', 9999);

$this->assertEquals(ELGG_HTTP_INTERNAL_SERVER_ERROR, $response->getStatusCode());
}

Expand Down
13 changes: 7 additions & 6 deletions engine/tests/phpunit/unit/Elgg/Http/OkResponseUnitTest.php
Expand Up @@ -6,18 +6,19 @@
* @group HttpService
* @group UnitTests
*/
class OkResponseUnitTest extends ResponseUnitTest {
class OkResponseUnitTest extends ResponseUnitTestCase {

public function up() {
$this->class = OkResponse::class;
public function getReponseClassName(): string {
return OkResponse::class;
}

public function testCanConstructWihtoutArguments() {
$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class();

$this->assertEquals('', $response->getContent());
$this->assertEquals(ELGG_HTTP_OK, $response->getStatusCode());
$this->assertEquals(null, $response->getForwardURL());
$this->assertNull($response->getForwardURL());
$this->assertEquals([], $response->getHeaders());
}

Expand All @@ -26,7 +27,7 @@ public function testCanConstructWithArguments() {
$status_code = ELGG_HTTP_PARTIAL_CONTENT;
$forward_url = REFERRER;

$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class($content, $status_code, $forward_url);

$this->assertEquals($content, $response->getContent());
Expand Down
11 changes: 6 additions & 5 deletions engine/tests/phpunit/unit/Elgg/Http/RedirectResponseUnitTest.php
Expand Up @@ -6,15 +6,16 @@
* @group HttpService
* @group UnitTests
*/
class RedirectResponseUnitTest extends ResponseUnitTest {
class RedirectResponseUnitTest extends ResponseUnitTestCase {

public function up() {
$this->class = RedirectResponse::class;
public function getReponseClassName(): string {
return RedirectResponse::class;
}

public function testCanConstructWihtoutArguments() {
$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class();

$this->assertEquals('', $response->getContent());
$this->assertEquals(ELGG_HTTP_FOUND, $response->getStatusCode());
$this->assertEquals(REFERRER, $response->getForwardURL());
Expand All @@ -25,7 +26,7 @@ public function testCanConstructWithArguments() {
$status_code = ELGG_HTTP_PERMANENTLY_REDIRECT;
$forward_url = REFERRER;

$test_class = $this->class;
$test_class = $this->getReponseClassName();
$response = new $test_class($forward_url, $status_code);

$this->assertEquals('', $response->getContent());
Expand Down

0 comments on commit 12204cc

Please sign in to comment.