<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.3.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Http;

use Cake\Http\Response as CakeResponse;
use Psr\Http\Message\ResponseInterface as PsrResponse;
use Zend\Diactoros\Response as DiactorosResponse;
use Zend\Diactoros\Stream;

/**
 * This class converts PSR7 responses into CakePHP ones and back again.
 *
 * By bridging the CakePHP and PSR7 responses together, applications
 * can be embedded as PSR7 middleware in a fully compatible way.
 *
 * @internal
 * @deprecated 3.4.0 No longer used. Will be removed in 4.0.0
 */
class ResponseTransformer
{
    /**
     * Convert a PSR7 Response into a CakePHP one.
     *
     * @param \Psr\Http\Message\ResponseInterface $response The response to convert.
     * @return \Cake\Http\Response The equivalent CakePHP response
     */
    public static function toCake(PsrResponse $response)
    {
        $body = static::getBody($response);
        $data = [
            'status' => $response->getStatusCode(),
            'body' => $body['body'],
        ];
        $cake = new CakeResponse($data);
        if ($body['file']) {
            $cake->file($body['file']);
        }
        $cookies = static::parseCookies($response->getHeader('Set-Cookie'));
        foreach ($cookies as $cookie) {
            $cake->cookie($cookie);
        }
        $headers = static::collapseHeaders($response);
        $cake->header($headers);

        if (!empty($headers['Content-Type'])) {
            $cake->type($headers['Content-Type']);
        }

        return $cake;
    }

    /**
     * Get the response body from a PSR7 Response.
     *
     * @param \Psr\Http\Message\ResponseInterface $response The response to convert.
     * @return array A hash of 'body' and 'file'
     */
    protected static function getBody(PsrResponse $response)
    {
        $stream = $response->getBody();
        if ($stream->getMetadata('wrapper_type') === 'plainfile') {
            return ['body' => '', 'file' => $stream->getMetadata('uri')];
        }
        if ($stream->getSize() === 0) {
            return ['body' => '', 'file' => false];
        }
        $stream->rewind();

        return ['body' => $stream->getContents(), 'file' => false];
    }

    /**
     * Parse the Set-Cookie headers in a PSR7 response
     * into the format CakePHP expects.
     *
     * @param array $cookieHeader A list of Set-Cookie headers.
     * @return array Parsed cookie data.
     */
    protected static function parseCookies(array $cookieHeader)
    {
        $cookies = [];
        foreach ($cookieHeader as $cookie) {
            if (strpos($cookie, '";"') !== false) {
                $cookie = str_replace('";"', '{__cookie_replace__}', $cookie);
                $parts = preg_split('/\;[ \t]*/', $cookie);
                $parts = str_replace('{__cookie_replace__}', '";"', $parts);
            } else {
                $parts = preg_split('/\;[ \t]*/', $cookie);
            }

            list($name, $value) = explode('=', array_shift($parts), 2);
            $parsed = ['name' => $name, 'value' => urldecode($value)];

            foreach ($parts as $part) {
                if (strpos($part, '=') !== false) {
                    list($key, $value) = explode('=', $part);
                } else {
                    $key = $part;
                    $value = true;
                }

                $key = strtolower($key);
                if ($key === 'httponly') {
                    $key = 'httpOnly';
                }
                if ($key === 'expires') {
                    $key = 'expire';
                    $value = strtotime($value);
                }
                if (!isset($parsed[$key])) {
                    $parsed[$key] = $value;
                }
            }
            $cookies[] = $parsed;
        }

        return $cookies;
    }

    /**
     * Convert a PSR7 Response headers into a flat array
     *
     * @param \Psr\Http\Message\ResponseInterface $response The response to convert.
     * @return array Headers.
     */
    protected static function collapseHeaders(PsrResponse $response)
    {
        $out = [];
        foreach ($response->getHeaders() as $name => $value) {
            if (count($value) === 1) {
                $out[$name] = $value[0];
            } else {
                $out[$name] = $value;
            }
        }

        return $out;
    }

    /**
     * Convert a CakePHP response into a PSR7 one.
     *
     * @param \Cake\Http\Response $response The CakePHP response to convert
     * @return \Psr\Http\Message\ResponseInterface $response The equivalent PSR7 response.
     */
    public static function toPsr(CakeResponse $response)
    {
        $status = $response->statusCode();
        $headers = $response->header();
        if (!isset($headers['Content-Type'])) {
            $headers = static::setContentType($headers, $response);
        }
        $cookies = $response->cookie();
        if ($cookies) {
            $headers['Set-Cookie'] = static::buildCookieHeader($cookies);
        }
        $stream = static::getStream($response);

        return new DiactorosResponse($stream, $status, $headers);
    }

    /**
     * Add in the Content-Type header if necessary.
     *
     * @param array $headers The headers to update
     * @param \Cake\Http\Response $response The CakePHP response to convert
     * @return array The updated headers.
     */
    protected static function setContentType($headers, $response)
    {
        if (isset($headers['Content-Type'])) {
            return $headers;
        }
        if (in_array($response->statusCode(), [204, 304])) {
            return $headers;
        }

        $whitelist = [
            'application/javascript', 'application/json', 'application/xml', 'application/rss+xml'
        ];

        $type = $response->type();
        $charset = $response->charset();

        $hasCharset = false;
        if ($charset && (strpos($type, 'text/') === 0 || in_array($type, $whitelist))) {
            $hasCharset = true;
        }

        $value = $type;
        if ($hasCharset) {
            $value = "{$type}; charset={$charset}";
        }
        $headers['Content-Type'] = $value;

        return $headers;
    }

    /**
     * Convert an array of cookies into header lines.
     *
     * @param array $cookies The cookies to serialize.
     * @return array A list of cookie header values.
     */
    protected static function buildCookieHeader($cookies)
    {
        $headers = [];
        foreach ($cookies as $cookie) {
            $parts = [
                sprintf('%s=%s', urlencode($cookie['name']), urlencode($cookie['value']))
            ];
            if ($cookie['expire']) {
                $cookie['expire'] = gmdate('D, d M Y H:i:s T', $cookie['expire']);
            }
            $attributes = [
                'expire' => 'Expires=%s',
                'path' => 'Path=%s',
                'domain' => 'Domain=%s',
                'httpOnly' => 'HttpOnly',
                'secure' => 'Secure',
            ];
            foreach ($attributes as $key => $attr) {
                if ($cookie[$key]) {
                    $parts[] = sprintf($attr, $cookie[$key]);
                }
            }
            $headers[] = implode('; ', $parts);
        }

        return $headers;
    }

    /**
     * Get the stream for the new response.
     *
     * @param \Cake\Http\Response $response The cake response to extract the body from.
     * @return \Psr\Http\Message\StreamInterface|string The stream.
     */
    protected static function getStream($response)
    {
        $stream = 'php://memory';
        $body = $response->body();
        if (is_string($body) && strlen($body)) {
            $stream = new Stream('php://memory', 'wb');
            $stream->write($body);

            return $stream;
        }
        if (is_callable($body)) {
            $stream = new CallbackStream($body);

            return $stream;
        }
        $file = $response->getFile();
        if ($file) {
            $stream = new Stream($file->path, 'rb');

            return $stream;
        }

        return $stream;
    }
}
