Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,9 @@ public function __construct(string $appName, array $urlParams = [], ?ServerConta

/** @deprecated 32.0.0 */
$this->registerDeprecatedAlias('Protocol', Http::class);
$this->registerService(Http::class, function (ContainerInterface $c) {
$protocol = $c->get(IRequest::class)->getHttpProtocol();
return new Http($_SERVER, $protocol);
});
$this->registerService(Http::class, fn (ContainerInterface $c)
=> new Http($c->get(IRequest::class)->getHttpProtocol())
);

/** @deprecated 32.0.0 */
$this->registerDeprecatedAlias('Dispatcher', Dispatcher::class);
Expand Down
165 changes: 79 additions & 86 deletions lib/private/AppFramework/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,100 +9,93 @@

use OCP\AppFramework\Http as BaseHttp;

/**
* Class for building HTTP status headers in Nextcloud.
*
* Provides protocol version handling and maps HTTP status codes to standard messages,
* used for generating accurate response headers within Nextcloud's AppFramework.
*/
class Http extends BaseHttp {
private $server;
private $protocolVersion;
protected $headers;

/**
* @param array $server $_SERVER
* @param string $protocolVersion the http version to use defaults to HTTP/1.1
*/
public function __construct($server, $protocolVersion = 'HTTP/1.1') {
$this->server = $server;
$this->protocolVersion = $protocolVersion;
private const STATUS_MESSAGES = [
self::STATUS_CONTINUE => 'Continue',
self::STATUS_SWITCHING_PROTOCOLS => 'Switching Protocols',
self::STATUS_PROCESSING => 'Processing',
self::STATUS_OK => 'OK',
self::STATUS_CREATED => 'Created',
self::STATUS_ACCEPTED => 'Accepted',
self:: STATUS_NON_AUTHORATIVE_INFORMATION => 'Non-Authorative Information',
self::STATUS_NO_CONTENT => 'No Content',
self::STATUS_RESET_CONTENT => 'Reset Content',
self::STATUS_PARTIAL_CONTENT => 'Partial Content',
self::STATUS_MULTI_STATUS => 'Multi-Status', // RFC 4918
self::STATUS_ALREADY_REPORTED => 'Already Reported', // RFC 5842
self::STATUS_IM_USED => 'IM Used', // RFC 3229
self:: STATUS_MULTIPLE_CHOICES => 'Multiple Choices',
self::STATUS_MOVED_PERMANENTLY => 'Moved Permanently',
self::STATUS_FOUND => 'Found',
self::STATUS_SEE_OTHER => 'See Other',
self::STATUS_NOT_MODIFIED => 'Not Modified',
self::STATUS_USE_PROXY => 'Use Proxy',
self::STATUS_RESERVED => 'Reserved',
self:: STATUS_TEMPORARY_REDIRECT => 'Temporary Redirect',
self::STATUS_BAD_REQUEST => 'Bad request',
self::STATUS_UNAUTHORIZED => 'Unauthorized',
self::STATUS_PAYMENT_REQUIRED => 'Payment Required',
self::STATUS_FORBIDDEN => 'Forbidden',
self:: STATUS_NOT_FOUND => 'Not Found',
self:: STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed',
self::STATUS_NOT_ACCEPTABLE => 'Not Acceptable',
self::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
self::STATUS_REQUEST_TIMEOUT => 'Request Timeout',
self::STATUS_CONFLICT => 'Conflict',
self::STATUS_GONE => 'Gone',
self::STATUS_LENGTH_REQUIRED => 'Length Required',
self::STATUS_PRECONDITION_FAILED => 'Precondition failed',
self::STATUS_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large',
self::STATUS_REQUEST_URI_TOO_LONG => 'Request-URI Too Long',
self::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
self::STATUS_REQUEST_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
self::STATUS_EXPECTATION_FAILED => 'Expectation Failed',
self::STATUS_IM_A_TEAPOT => 'I\'m a teapot', // RFC 2324
self::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', // RFC 4918
self::STATUS_LOCKED => 'Locked', // RFC 4918
self::STATUS_FAILED_DEPENDENCY => 'Failed Dependency', // RFC 4918
self::STATUS_UPGRADE_REQUIRED => 'Upgrade required',
self::STATUS_PRECONDITION_REQUIRED => 'Precondition required', // draft-nottingham-http-new-status
self::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests', // draft-nottingham-http-new-status
self::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', // draft-nottingham-http-new-status
self::STATUS_INTERNAL_SERVER_ERROR => 'Internal Server Error',
self::STATUS_NOT_IMPLEMENTED => 'Not Implemented',
self::STATUS_BAD_GATEWAY => 'Bad Gateway',
self::STATUS_SERVICE_UNAVAILABLE => 'Service Unavailable',
self::STATUS_GATEWAY_TIMEOUT => 'Gateway Timeout',
self:: STATUS_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version not supported',
self::STATUS_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',
self::STATUS_INSUFFICIENT_STORAGE => 'Insufficient Storage', // RFC 4918
self::STATUS_LOOP_DETECTED => 'Loop Detected', // RFC 5842
self::STATUS_BANDWIDTH_LIMIT_EXCEEDED => 'Bandwidth Limit Exceeded', // non-standard
self::STATUS_NOT_EXTENDED => 'Not extended',
self::STATUS_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', // draft-nottingham-http-new-status
];

$this->headers = [
self::STATUS_CONTINUE => 'Continue',
self::STATUS_SWITCHING_PROTOCOLS => 'Switching Protocols',
self::STATUS_PROCESSING => 'Processing',
self::STATUS_OK => 'OK',
self::STATUS_CREATED => 'Created',
self::STATUS_ACCEPTED => 'Accepted',
self::STATUS_NON_AUTHORATIVE_INFORMATION => 'Non-Authorative Information',
self::STATUS_NO_CONTENT => 'No Content',
self::STATUS_RESET_CONTENT => 'Reset Content',
self::STATUS_PARTIAL_CONTENT => 'Partial Content',
self::STATUS_MULTI_STATUS => 'Multi-Status', // RFC 4918
self::STATUS_ALREADY_REPORTED => 'Already Reported', // RFC 5842
self::STATUS_IM_USED => 'IM Used', // RFC 3229
self::STATUS_MULTIPLE_CHOICES => 'Multiple Choices',
self::STATUS_MOVED_PERMANENTLY => 'Moved Permanently',
self::STATUS_FOUND => 'Found',
self::STATUS_SEE_OTHER => 'See Other',
self::STATUS_NOT_MODIFIED => 'Not Modified',
self::STATUS_USE_PROXY => 'Use Proxy',
self::STATUS_RESERVED => 'Reserved',
self::STATUS_TEMPORARY_REDIRECT => 'Temporary Redirect',
self::STATUS_BAD_REQUEST => 'Bad request',
self::STATUS_UNAUTHORIZED => 'Unauthorized',
self::STATUS_PAYMENT_REQUIRED => 'Payment Required',
self::STATUS_FORBIDDEN => 'Forbidden',
self::STATUS_NOT_FOUND => 'Not Found',
self::STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed',
self::STATUS_NOT_ACCEPTABLE => 'Not Acceptable',
self::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
self::STATUS_REQUEST_TIMEOUT => 'Request Timeout',
self::STATUS_CONFLICT => 'Conflict',
self::STATUS_GONE => 'Gone',
self::STATUS_LENGTH_REQUIRED => 'Length Required',
self::STATUS_PRECONDITION_FAILED => 'Precondition failed',
self::STATUS_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large',
self::STATUS_REQUEST_URI_TOO_LONG => 'Request-URI Too Long',
self::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
self::STATUS_REQUEST_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
self::STATUS_EXPECTATION_FAILED => 'Expectation Failed',
self::STATUS_IM_A_TEAPOT => 'I\'m a teapot', // RFC 2324
self::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', // RFC 4918
self::STATUS_LOCKED => 'Locked', // RFC 4918
self::STATUS_FAILED_DEPENDENCY => 'Failed Dependency', // RFC 4918
self::STATUS_UPGRADE_REQUIRED => 'Upgrade required',
self::STATUS_PRECONDITION_REQUIRED => 'Precondition required', // draft-nottingham-http-new-status
self::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests', // draft-nottingham-http-new-status
self::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', // draft-nottingham-http-new-status
self::STATUS_INTERNAL_SERVER_ERROR => 'Internal Server Error',
self::STATUS_NOT_IMPLEMENTED => 'Not Implemented',
self::STATUS_BAD_GATEWAY => 'Bad Gateway',
self::STATUS_SERVICE_UNAVAILABLE => 'Service Unavailable',
self::STATUS_GATEWAY_TIMEOUT => 'Gateway Timeout',
self::STATUS_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version not supported',
self::STATUS_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',
self::STATUS_INSUFFICIENT_STORAGE => 'Insufficient Storage', // RFC 4918
self::STATUS_LOOP_DETECTED => 'Loop Detected', // RFC 5842
self::STATUS_BANDWIDTH_LIMIT_EXCEEDED => 'Bandwidth Limit Exceeded', // non-standard
self::STATUS_NOT_EXTENDED => 'Not extended',
self::STATUS_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', // draft-nottingham-http-new-status
];
public function __construct(
private readonly string $protocolVersion = 'HTTP/1.1',
) {
}


/**
* Gets the correct header
* @param int Http::CONSTANT $status the constant from the Http class
* @param \DateTime $lastModified formatted last modified date
* @param string $ETag the etag
* @return string
* Gets the correct status header line.
*
* @param int $status HTTP status code constant
* @return string Header string like "HTTP/1.1 200 OK"
*/
public function getStatusHeader($status) {
// we have one change currently for the http 1.0 header that differs
// from 1.1: STATUS_TEMPORARY_REDIRECT should be STATUS_FOUND
// if this differs any more, we want to create childclasses for this
if ($status === self::STATUS_TEMPORARY_REDIRECT
&& $this->protocolVersion === 'HTTP/1.0') {
public function getStatusHeader(int $status): string {
// If HTTP/1.0, 307 Temporary Redirect should be 302 Found for compliance.
if ($this->protocolVersion === 'HTTP/1.0' && $status === self::STATUS_TEMPORARY_REDIRECT) {
$status = self::STATUS_FOUND;
}
$message = self::STATUS_MESSAGES[$status] ?? 'Unknown Status';

return $this->protocolVersion . ' ' . $status . ' '
. $this->headers[$status];
return $this->protocolVersion . ' ' . $status . ' ' . $message;
}
}
10 changes: 7 additions & 3 deletions tests/lib/AppFramework/Http/DispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,13 @@ protected function setUp(): void {
* @param string $out
* @param string $httpHeaders
*/
private function setMiddlewareExpectations($out = null,
$httpHeaders = null, $responseHeaders = [],
$ex = false, $catchEx = true) {
private function setMiddlewareExpectations(
$out = null,
$httpHeaders = '',
$responseHeaders = [],
$ex = false,
$catchEx = true,
) {
if ($ex) {
$exception = new \Exception();
$this->middlewareDispatcher->expects($this->once())
Expand Down
59 changes: 29 additions & 30 deletions tests/lib/AppFramework/Http/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,39 @@
namespace Test\AppFramework\Http;

use OC\AppFramework\Http;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;

/**
* Unit tests for OC\AppFramework\Http.
*/
class HttpTest extends \Test\TestCase {
private $server;

/**
* @var Http
*/
private $http;

protected function setUp(): void {
parent::setUp();

$this->server = [];
$this->http = new Http($this->server);
#[Test]
#[DataProvider('statusHeaderProvider')]
public function testGetStatusHeader(string $protocol, int $statusCode, string $expectedHeader): void {
$http = new Http($protocol);
$header = $http->getStatusHeader($statusCode);
$this->assertEquals($expectedHeader, $header);
}


public function testProtocol(): void {
$header = $this->http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT);
$this->assertEquals('HTTP/1.1 307 Temporary Redirect', $header);
}


public function testProtocol10(): void {
$this->http = new Http($this->server, 'HTTP/1.0');
$header = $this->http->getStatusHeader(Http::STATUS_OK);
$this->assertEquals('HTTP/1.0 200 OK', $header);
}

public function testTempRedirectBecomesFoundInHttp10(): void {
$http = new Http([], 'HTTP/1.0');

$header = $http->getStatusHeader(Http::STATUS_TEMPORARY_REDIRECT);
$this->assertEquals('HTTP/1.0 302 Found', $header);
public static function statusHeaderProvider(): array {
return [
// Standard OK
['HTTP/1.1', Http::STATUS_OK, 'HTTP/1.1 200 OK'],
// 307 is unchanged for HTTP/1.1
['HTTP/1.1', Http::STATUS_TEMPORARY_REDIRECT, 'HTTP/1.1 307 Temporary Redirect'],
// 307 maps to 302 for HTTP/1.0
['HTTP/1.0', Http::STATUS_TEMPORARY_REDIRECT, 'HTTP/1.0 302 Found'],
// Not Found
['HTTP/1.1', Http::STATUS_NOT_FOUND, 'HTTP/1.1 404 Not Found'],
// Forbidden
['HTTP/1.1', Http::STATUS_FORBIDDEN, 'HTTP/1.1 403 Forbidden'],
// Bad Request
['HTTP/1.1', Http::STATUS_BAD_REQUEST, 'HTTP/1.1 400 Bad request'],
// Unknown/Fallback
['HTTP/1.1', 999, 'HTTP/1.1 999 Unknown Status'],
['HTTP/2.0', 123, 'HTTP/2.0 123 Unknown Status'],
];
}
// TODO: write unittests for http codes
}
Loading