File size: 4,313 Bytes
d2897cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?php

namespace MauticPlugin\MauticFocusBundle\Helper;

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Check if URL can be displayed via IFRAME.
 */
class IframeAvailabilityChecker
{
    public function __construct(
        private TranslatorInterface $translator
    ) {
    }

    public function check(string $url, string $currentScheme): JsonResponse
    {
        $response        = new JsonResponse();
        $responseContent = [
            'status'       => 0,
            'errorMessage' => '',
        ];

        if ($this->checkProtocolMismatch($url, $currentScheme)) {
            $responseContent['errorMessage'] = $this->translator->trans(
                'mautic.focus.protocol.mismatch',
                [
                    '%url%' => str_replace('http://', 'https://', $url),
                ]);
        } else {
            $client = HttpClient::create([
                'headers' => [
                    'User-Agent' => 'Mautic',
                ],
            ]);

            try {
                /** @var ResponseInterface $httpResponse */
                $httpResponse = $client->request(Request::METHOD_GET, $url);

                $blockingHeader = $this->checkHeaders($httpResponse->getHeaders(false));

                if ('' !== $blockingHeader) {
                    $responseContent['errorMessage'] = $this->translator->trans(
                        'mautic.focus.blocking.iframe.header',
                        [
                            '%url%'    => $url,
                            '%header%' => $blockingHeader,
                        ]
                    );
                }
            } catch (\Exception $e) {
                // Transport exception with SSL cert for example
                $responseContent['errorMessage'] = $e->getMessage();
            }
        }

        if ('' === $responseContent['errorMessage'] && Response::HTTP_OK === $httpResponse->getStatusCode()) {
            $responseContent['status'] = 1;
        }

        $response->setData($responseContent);

        return $response;
    }

    /**
     * Iframe doesn't allow cross protocol requests.
     */
    private function checkProtocolMismatch(string $url, string $currentScheme): bool
    {
        // Mixed Content: The page at 'https://example.com' was loaded over HTTPS,
        // but requested an insecure frame 'http://target-example.com/'. This request has been blocked; the content
        // must be served over HTTPS.
        return 'https' === $currentScheme && str_starts_with($url, 'http://');
    }

    /**
     * @param array $headers Content of Symfony\Contracts\HttpClient\ResponseInterface::getHeaders()
     *
     * @return string Blocking header if problem found
     */
    private function checkHeaders(array $headers): string
    {
        $return = '';

        if ($this->headerContains($headers, 'x-frame-options')) {
            // @see https://stackoverflow.com/questions/31944552/iframe-refuses-to-display
            $return = 'x-frame-options: SAMEORIGIN';
        }

        if ($this->headerContains($headers, 'content-security-policy', "frame-ancestors 'self'")) {
            // https://seznam.cz
            // Refused to display 'https://www.seznam.cz/' in a frame because an ancestor violates the following
            // Content Security Policy directive: "frame-ancestors 'self'".
            // @see https://stackoverflow.com/questions/31944552/iframe-refuses-to-display
            $return = 'content-security-policy';
        }

        return $return;
    }

    private function headerContains(array $headers, string $name, string $content = null): bool
    {
        $headers = array_change_key_case($headers, CASE_LOWER);

        if (array_key_exists($name, $headers)) {
            if (null !== $content) {
                if (str_starts_with($headers[$name][0], $content)) {
                    return true;
                } else {
                    return false;
                }
            }

            return true;
        }

        return false;
    }
}