1 | <?php |
||
2 | /* |
||
3 | * @copyright 2020 Mautic, Inc. All rights reserved |
||
4 | * @author Mautic, Inc. |
||
5 | * |
||
6 | * @link https://mautic.com |
||
7 | * |
||
8 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||
9 | */ |
||
10 | |||
11 | namespace MauticPlugin\MauticFocusBundle\Helper; |
||
12 | |||
13 | use Symfony\Component\HttpClient\HttpClient; |
||
14 | use Symfony\Component\HttpFoundation\JsonResponse; |
||
15 | use Symfony\Component\HttpFoundation\Request; |
||
16 | use Symfony\Component\HttpFoundation\Response; |
||
17 | use Symfony\Component\Translation\TranslatorInterface; |
||
18 | use Symfony\Contracts\HttpClient\ResponseInterface; |
||
19 | |||
20 | /** |
||
21 | * Check if URL can be displayed via IFRAME. |
||
22 | */ |
||
23 | class IframeAvailabilityChecker |
||
24 | { |
||
25 | /** |
||
26 | * @var TranslatorInterface |
||
27 | */ |
||
28 | private $translator; |
||
29 | |||
30 | public function __construct(TranslatorInterface $translator) |
||
31 | { |
||
32 | $this->translator = $translator; |
||
33 | } |
||
34 | |||
35 | public function check(string $url, string $currentScheme): JsonResponse |
||
36 | { |
||
37 | $response = new JsonResponse(); |
||
38 | $responseContent = [ |
||
39 | 'status' => 0, |
||
40 | 'errorMessage' => '', |
||
41 | ]; |
||
42 | |||
43 | if ($this->checkProtocolMismatch($url, $currentScheme)) { |
||
44 | $responseContent['errorMessage'] = $this->translator->trans( |
||
45 | 'mautic.focus.protocol.mismatch', |
||
46 | [ |
||
47 | '%url%' => str_replace('http://', 'https://', $url), |
||
48 | ]); |
||
49 | } else { |
||
50 | $client = HttpClient::create([ |
||
51 | 'headers' => [ |
||
52 | 'User-Agent' => 'Mautic', |
||
53 | ], |
||
54 | ]); |
||
55 | |||
56 | try { |
||
57 | /** @var ResponseInterface $httpResponse */ |
||
58 | $httpResponse = $client->request(Request::METHOD_GET, $url); |
||
59 | |||
60 | $blockingHeader = $this->checkHeaders($httpResponse->getHeaders(false)); |
||
61 | |||
62 | if ('' !== $blockingHeader) { |
||
63 | $responseContent['errorMessage'] = $this->translator->trans( |
||
64 | 'mautic.focus.blocking.iframe.header', |
||
65 | [ |
||
66 | '%url%' => $url, |
||
67 | '%header%' => $blockingHeader, |
||
68 | ] |
||
69 | ); |
||
70 | } |
||
71 | } catch (\Exception $e) { |
||
72 | // Transport exception with SSL cert for example |
||
73 | $responseContent['errorMessage'] = $e->getMessage(); |
||
74 | } |
||
75 | } |
||
76 | |||
77 | if ('' === $responseContent['errorMessage'] && Response::HTTP_OK === $httpResponse->getStatusCode()) { |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
78 | $responseContent['status'] = 1; |
||
79 | } |
||
80 | |||
81 | $response->setData($responseContent); |
||
82 | |||
83 | return $response; |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Iframe doesn't allow cross protocol requests. |
||
88 | */ |
||
89 | private function checkProtocolMismatch(string $url, string $currentScheme): bool |
||
90 | { |
||
91 | // Mixed Content: The page at 'https://example.com' was loaded over HTTPS, |
||
92 | // but requested an insecure frame 'http://target-example.com/'. This request has been blocked; the content |
||
93 | // must be served over HTTPS. |
||
94 | return 'https' === $currentScheme && 0 === strpos($url, 'http://'); |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @param array $headers Content of Symfony\Contracts\HttpClient\ResponseInterface::getHeaders() |
||
99 | * |
||
100 | * @return string Blocking header if problem found |
||
101 | */ |
||
102 | private function checkHeaders(array $headers): string |
||
103 | { |
||
104 | $return = ''; |
||
105 | |||
106 | if ($this->headerContains($headers, 'x-frame-options')) { |
||
107 | // @see https://stackoverflow.com/questions/31944552/iframe-refuses-to-display |
||
108 | $return = 'x-frame-options: SAMEORIGIN'; |
||
109 | } |
||
110 | |||
111 | if ($this->headerContains($headers, 'content-security-policy', "frame-ancestors 'self'")) { |
||
112 | // https://seznam.cz |
||
113 | // Refused to display 'https://www.seznam.cz/' in a frame because an ancestor violates the following |
||
114 | // Content Security Policy directive: "frame-ancestors 'self'". |
||
115 | // @see https://stackoverflow.com/questions/31944552/iframe-refuses-to-display |
||
116 | $return = 'content-security-policy'; |
||
117 | } |
||
118 | |||
119 | return $return; |
||
120 | } |
||
121 | |||
122 | private function headerContains(array $headers, string $name, string $content = null): bool |
||
123 | { |
||
124 | $headers = array_change_key_case($headers, CASE_LOWER); |
||
125 | |||
126 | if (array_key_exists($name, $headers)) { |
||
127 | if (null !== $content) { |
||
128 | if (0 === strpos($headers[$name][0], $content)) { |
||
129 | return true; |
||
130 | } else { |
||
131 | return false; |
||
132 | } |
||
133 | } |
||
134 | |||
135 | return true; |
||
136 | } |
||
137 | |||
138 | return false; |
||
139 | } |
||
140 | } |
||
141 |