Merge pull request #19438 from colemanw/afformDropAttrSupport
[civicrm-core.git] / CRM / Utils / SameSite.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * SameSite Utility Class.
14 *
15 * Determines if the current User Agent can handle the `SameSite=None` parameter
16 * by mapping against known incompatible clients.
17 *
18 * Sample code:
19 *
20 * // Get User Agent string.
21 * $rawUserAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
22 * $userAgent = mb_convert_encoding($rawUserAgent, 'UTF-8');
23 *
24 * // Get boolean representing User Agent compatibility.
25 * $shouldUseSameSite = CRM_Utils_SameSite::shouldSendSameSiteNone($userAgent);
26 *
27 * Based on code provided by "The Chromium Projects".
28 *
29 * @see https://www.chromium.org/updates/same-site/incompatible-clients
30 */
31 class CRM_Utils_SameSite {
32
33 /**
34 * Determine if the current User Agent can handle the `SameSite=None` parameter.
35 *
36 * @param str $userAgent The User Agent.
37 * @return bool True if the User Agent is compatible, FALSE otherwise.
38 */
39 public static function shouldSendSameSiteNone($userAgent) {
40 return !self::isSameSiteNoneIncompatible($userAgent);
41 }
42
43 /**
44 * Detect classes of browsers known to be incompatible.
45 *
46 * @param str $userAgent The User Agent.
47 * @return bool True if the User Agent is determined to be incompatible, FALSE otherwise.
48 */
49 private static function isSameSiteNoneIncompatible($userAgent) {
50 return self::hasWebKitSameSiteBug($userAgent) ||
51 self::dropsUnrecognizedSameSiteCookies($userAgent);
52 }
53
54 /**
55 * Detect versions of Safari and embedded browsers on MacOS 10.14 and all
56 * browsers on iOS 12.
57 *
58 * These versions will erroneously treat cookies marked with `SameSite=None`
59 * as if they were marked `SameSite=Strict`.
60 *
61 * @param str $userAgent The User Agent.
62 * @return bool
63 */
64 private static function hasWebKitSameSiteBug($userAgent) {
65 return self::isIosVersion(12, $userAgent) || (self::isMacosxVersion(10, 14, $userAgent) &&
66 (self::isSafari($userAgent) || self::isMacEmbeddedBrowser($userAgent)));
67 }
68
69 /**
70 * Detect versions of UC Browser on Android prior to version 12.13.2.
71 *
72 * Older versions will reject a cookie with `SameSite=None`. This behavior was
73 * correct according to the version of the cookie specification at that time,
74 * but with the addition of the new "None" value to the specification, this
75 * behavior has been updated in newer versions of UC Browser.
76 *
77 * @param str $userAgent The User Agent.
78 * @return bool
79 */
80 private static function dropsUnrecognizedSameSiteCookies($userAgent) {
81 if (self::isUcBrowser($userAgent)) {
82 return !self::isUcBrowserVersionAtLeast(12, 13, 2, $userAgent);
83 }
84
85 return self::isChromiumBased($userAgent) &&
86 self::isChromiumVersionAtLeast(51, $userAgent, '>=') &&
87 self::isChromiumVersionAtLeast(67, $userAgent, '<=');
88 }
89
90 /**
91 * Detect iOS version.
92 *
93 * @param int $major The major version to test.
94 * @param str $userAgent The User Agent.
95 * @return bool
96 */
97 private static function isIosVersion($major, $userAgent) {
98 $regex = "/\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\//";
99 $matched = [];
100
101 if (preg_match($regex, $userAgent, $matched)) {
102 // Extract digits from first capturing group.
103 $version = (int) $matched[1];
104 return version_compare($version, $major, '<=');
105 }
106
107 return FALSE;
108 }
109
110 /**
111 * Detect MacOS version.
112 *
113 * @param int $major The major version to test.
114 * @param int $minor The minor version to test.
115 * @param str $userAgent The User Agent.
116 * @return bool
117 */
118 private static function isMacosxVersion($major, $minor, $userAgent) {
119 $regex = "/\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\//";
120 $matched = [];
121
122 if (preg_match($regex, $userAgent, $matched)) {
123 // Extract digits from first and second capturing groups.
124 return version_compare((int) $matched[1], $major, '=') &&
125 version_compare((int) $matched[2], $minor, '<=');
126 }
127
128 return FALSE;
129 }
130
131 /**
132 * Detect MacOS Safari.
133 *
134 * @param str $userAgent The User Agent.
135 * @return bool
136 */
137 private static function isSafari($userAgent) {
138 $regex = "/Version\/.* Safari\//";
139 return preg_match($regex, $userAgent) && !self::isChromiumBased($userAgent);
140 }
141
142 /**
143 * Detect MacOS embedded browser.
144 *
145 * @param str $userAgent The User Agent.
146 * @return FALSE|int
147 */
148 private static function isMacEmbeddedBrowser($userAgent) {
149 $regex = "/^Mozilla\/[\.\d]+ \(Macintosh;.*Mac OS X [_\d]+\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$/";
150 return preg_match($regex, $userAgent);
151 }
152
153 /**
154 * Detect if browser is Chromium based.
155 *
156 * @param str $userAgent The User Agent.
157 * @return FALSE|int
158 */
159 private static function isChromiumBased($userAgent) {
160 $regex = "/Chrom(e|ium)/";
161 return preg_match($regex, $userAgent);
162 }
163
164 /**
165 * Detect if Chromium version meets requirements.
166 *
167 * @param int $major The major version to test.
168 * @param str $userAgent The User Agent.
169 * @param str $operator
170 * @return bool|int
171 */
172 private static function isChromiumVersionAtLeast($major, $userAgent, $operator) {
173 $regex = "/Chrom[^ \/]+\/(\d+)[\.\d]* /";
174 $matched = [];
175
176 if (preg_match($regex, $userAgent, $matched)) {
177 // Extract digits from first capturing group.
178 $version = (int) $matched[1];
179 return version_compare($version, $major, $operator);
180 }
181 return FALSE;
182 }
183
184 /**
185 * Detect UCBrowser.
186 *
187 * @param str $userAgent The User Agent.
188 * @return FALSE|int
189 */
190 private static function isUcBrowser($userAgent) {
191 $regex = "/UCBrowser\//";
192 return preg_match($regex, $userAgent);
193 }
194
195 /**
196 * Detect if UCBrowser version meets requirements.
197 *
198 * @param int $major The major version to test.
199 * @param int $minor The minor version to test.
200 * @param int $build The build version to test.
201 * @param str $userAgent The User Agent.
202 * @return bool|int
203 */
204 private static function isUcBrowserVersionAtLeast($major, $minor, $build, $userAgent) {
205 $regex = "/UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* /";
206 $matched = [];
207
208 if (preg_match($regex, $userAgent, $matched)) {
209 // Extract digits from three capturing groups.
210 $majorVersion = (int) $matched[1];
211 $minorVersion = (int) $matched[2];
212 $buildVersion = (int) $matched[3];
213
214 if (version_compare($majorVersion, $major, '>=')) {
215 if (version_compare($minorVersion, $minor, '>=')) {
216 return version_compare($buildVersion, $build, '>=');
217 }
218 }
219 }
220
221 return FALSE;
222 }
223
224 }