Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
169 changes: 81 additions & 88 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;
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
];

/**
* @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;

$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
*/
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') {
/**
* 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(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;
}
}
64 changes: 32 additions & 32 deletions tests/lib/AppFramework/Http/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,40 @@
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);
}


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);
#[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 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