<?php

/**
 * Class CachedRemoteGetRequest.
 *
 * @package AmpProject\AmpWP
 */
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest;

use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetCachedResponse;
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetFromRemoteUrl;
use Google\Web_Stories_Dependencies\AmpProject\RemoteGetRequest;
use Google\Web_Stories_Dependencies\AmpProject\RemoteRequest\RemoteGetRequestResponse;
use Google\Web_Stories_Dependencies\AmpProject\Response;
use DateTimeImmutable;
use DateTimeInterface;
/**
 * Caching decorator for RemoteGetRequest implementations.
 *
 * Caching uses WordPress transients.
 *
 * @package AmpProject\AmpWP
 * @since 2.0
 * @internal
 */
final class CachedRemoteGetRequest implements RemoteGetRequest
{
    /**
     * Prefix to use to identify transients.
     *
     * @var string
     */
    const TRANSIENT_PREFIX = 'amp_remote_request_';
    /**
     * Cache control header directive name.
     *
     * @var string
     */
    const CACHE_CONTROL = 'Cache-Control';
    /**
     * Remote request object to decorate with caching.
     *
     * @var RemoteGetRequest
     */
    private $remote_request;
    /**
     * Cache expiration time in seconds.
     *
     * This will be used by default for successful requests when the 'cache-control: max-age' was not provided.
     *
     * @var int
     */
    private $expiry;
    /**
     * Minimum cache expiration time in seconds.
     *
     * This will be used for failed requests, or for successful requests when the 'cache-control: max-age' is inferior.
     * Caching will never expire quicker than this minimum.
     *
     * @var int
     */
    private $min_expiry;
    /**
     * Whether to use Cache-Control headers to decide on expiry times if available.
     *
     * @var bool
     */
    private $use_cache_control;
    /**
     * Instantiate a CachedRemoteGetRequest object.
     *
     * This is a decorator that can wrap around an existing remote request object to add a caching layer.
     *
     * @param RemoteGetRequest $remote_request    Remote request object to decorate with caching.
     * @param int|float        $expiry            Optional. Default cache expiry in seconds. Defaults to 30 days.
     * @param int|float        $min_expiry        Optional. Default enforced minimum cache expiry in seconds. Defaults
     *                                            to 24 hours.
     * @param bool             $use_cache_control Optional. Use Cache-Control headers for expiry if available. Defaults
     *                                            to true.
     */
    public function __construct(RemoteGetRequest $remote_request, $expiry = \MONTH_IN_SECONDS, $min_expiry = \DAY_IN_SECONDS, $use_cache_control = \true)
    {
        $this->remote_request = $remote_request;
        $this->expiry = (int) $expiry;
        $this->min_expiry = (int) $min_expiry;
        $this->use_cache_control = (bool) $use_cache_control;
    }
    /**
     * Do a GET request to retrieve the contents of a remote URL.
     *
     * @todo Should this also respect additional Cache-Control directives like 'no-cache'?
     *
     * @param string $url     URL to get.
     * @param array  $headers Optional. Associative array of headers to send with the request. Defaults to empty array.
     * @return Response Response for the executed request.
     * @throws FailedToGetCachedResponse If retrieving the contents from the URL failed.
     */
    public function get($url, $headers = [])
    {
        $cache_key = self::TRANSIENT_PREFIX . \md5(__CLASS__ . $url);
        $cached_response = \get_transient($cache_key);
        $headers = [];
        if (\is_string($cached_response)) {
            if (\PHP_MAJOR_VERSION >= 7) {
                $cached_response = \unserialize($cached_response, [CachedResponse::class, DateTimeImmutable::class]);
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
            } else {
                // PHP 5.6 does not provide the second $options argument yet.
                $cached_response = \unserialize($cached_response);
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
            }
        }
        if (!$cached_response instanceof CachedResponse || $cached_response->is_expired()) {
            try {
                $response = $this->remote_request->get($url, $headers);
                $status = $response->getStatusCode();
                /** @var DateTimeImmutable $expiry */
                $expiry = $this->get_expiry_time($response);
                $headers = $response->getHeaders();
                $body = $response->getBody();
            } catch (FailedToGetFromRemoteUrl $exception) {
                $status = $exception->getStatusCode();
                $expiry = new DateTimeImmutable("+ {$this->min_expiry} seconds");
                $body = $exception->getMessage();
            }
            $cached_response = new CachedResponse($body, $headers, $status, $expiry);
            // Transient extend beyond cache expiry to allow for serving stale content.
            // @TODO: We don't serve stale content atm, but rather synchronously refresh.
            // See https://github.com/ampproject/amp-wp/issues/5477.
            $transient_expiry = $expiry->modify("+ {$this->expiry} seconds");
            \set_transient($cache_key, \serialize($cached_response), $transient_expiry->getTimestamp());
            // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
        }
        if (!$cached_response->is_valid()) {
            throw new FailedToGetCachedResponse($url);
        }
        return new RemoteGetRequestResponse($cached_response->get_body(), $cached_response->get_headers(), $cached_response->get_status_code());
    }
    /**
     * Get the expiry time of the data to cache.
     *
     * This will use the cache-control header information in the provided response or fall back to the provided default
     * expiry.
     *
     * @param Response $response Response object to get the expiry from.
     * @return DateTimeInterface Expiry of the data.
     */
    private function get_expiry_time(Response $response)
    {
        if ($this->use_cache_control && $response->hasHeader(self::CACHE_CONTROL)) {
            $expiry = \max($this->min_expiry, $this->get_max_age($response->getHeader(self::CACHE_CONTROL)));
            return new DateTimeImmutable("+ {$expiry} seconds");
        }
        return new DateTimeImmutable("+ {$this->expiry} seconds");
    }
    /**
     * Get the max age setting from one or more cache-control header strings.
     *
     * @param array|string $cache_control_strings One or more cache control header strings.
     * @return int Value of the max-age cache directive. 0 if not found.
     */
    private function get_max_age($cache_control_strings)
    {
        $max_age = 0;
        foreach ((array) $cache_control_strings as $cache_control_string) {
            $cache_control_parts = \array_map('trim', \explode(',', $cache_control_string));
            foreach ($cache_control_parts as $cache_control_part) {
                $cache_control_setting_parts = \array_map('trim', \explode('=', $cache_control_part));
                if (\count($cache_control_setting_parts) !== 2) {
                    continue;
                }
                if ('max-age' === $cache_control_setting_parts[0]) {
                    $max_age = \absint($cache_control_setting_parts[1]);
                }
            }
        }
        return $max_age;
    }
}
