diff --git a/src/Http/Http.php b/src/Http/Http.php index 3a091c6a..68713c82 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -49,6 +49,9 @@ class Http extends Base protected string|null $requestClass = null; protected string|null $responseClass = null; + protected bool $compression = false; + protected int $compressionLevel = 0; + /** * Http * @@ -79,6 +82,15 @@ public function setRequestClass(string $requestClass) $this->requestClass = $requestClass; } + /** + * Set Compression + */ + public function setCompression(bool $compression, int $compressionLevel = 0) + { + $this->compression = $compression; + $this->compressionLevel = $compressionLevel; + } + /** * GET * @@ -317,6 +329,11 @@ public function start() if (!\is_null($this->responseClass)) { $response = new $this->responseClass($response); + + if ($this->compression) { + $response->setAcceptEncoding($request->getHeader('accept-encoding') ?? ''); + $response->setCompressionLevel($this->compressionLevel ?? 1); + } } $context = clone $this->container; diff --git a/src/Http/Response.php b/src/Http/Response.php index 66a0eeaa..72cce0f2 100755 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -245,6 +245,16 @@ abstract class Response */ protected int $size = 0; + /** + * @var string + */ + protected string $acceptEncoding = ''; + + /** + * @var int + */ + protected int $compressionLevel = 0; + /** * Response constructor. * @@ -270,6 +280,19 @@ public function setContentType(string $type, string $charset = ''): static return $this; } + /** + * Set accept encoding + * + * Set HTTP accept encoding header. + * + * @param string $acceptEncoding + */ + public function setAcceptEncoding(string $acceptEncoding): static + { + $this->acceptEncoding = $acceptEncoding; + return $this; + } + /** * Get content type * @@ -459,6 +482,25 @@ public function getCookies(): array return $this->cookies; } + public function getEncoding(): ?string + { + if (empty($this->acceptEncoding || isset($this->compressed[$this->contentType]))) { + return null; + } + + if (strpos($this->acceptEncoding, 'br') !== false && function_exists('brotli_compress')) { + return 'br'; + } + + if (strpos($this->acceptEncoding, 'gzip') !== false && function_exists('gzencode')) { + return 'gzip'; + } + + if (strpos($this->acceptEncoding, 'deflate') !== false && function_exists('gzdeflate')) { + return 'deflate'; + } + } + /** * Output response * @@ -475,37 +517,51 @@ public function send(string $body = ''): void $this->sent = true; - $this - ->addHeader('Server', array_key_exists('Server', $this->headers) ? $this->headers['Server'] : 'Utopia/Http') - ->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime)) - ; - - $this - ->appendCookies() - ->appendHeaders(); - - if (!$this->disablePayload) { - $length = strlen($body); + $serverHeader = $this->headers['Server'] ?? 'Utopia/Http'; + $this->addHeader('Server', $serverHeader); + $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime)); - $this->size = $this->size + strlen(implode("\n", $this->headers)) + $length; + $this->appendCookies()->appendHeaders(); - if (array_key_exists( - $this->contentType, - $this->compressed - ) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb - $this->end($body); - } else { - for ($i = 0; $i < ceil($length / self::CHUNK_SIZE); $i++) { - $this->write(substr($body, ($i * self::CHUNK_SIZE), min(self::CHUNK_SIZE, $length - ($i * self::CHUNK_SIZE)))); - } + // Send response + if ($this->disablePayload) { + $this->end(); + return; + } - $this->end(); + // Compress body + $encoding = $this->getEncoding(); + if ($encoding) { + switch($encoding) { + case 'br': + $body = brotli_compress($body, $this->compressionLevel); + break; + case 'gzip': + $body = gzencode($body, $this->compressionLevel); + break; + case 'deflate': + $body = gzdeflate($body, $this->compressionLevel); + break; } + $this->addHeader('Content-Encoding', $encoding); + $this->addHeader('Vary', 'Accept-Encoding'); + } + + $headerSize = strlen(implode("\n", $this->headers)); + $bodyLength = strlen($body); + $this->size += $headerSize + $bodyLength; - $this->disablePayload(); + if ($bodyLength <= self::CHUNK_SIZE) { + $this->end($body); } else { + $chunks = str_split($body, self::CHUNK_SIZE); + foreach ($chunks as $chunk) { + $this->write($chunk); + } $this->end(); } + + $this->disablePayload(); } /**