3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * SameSite Utility Class.
15 * Determines if the current User Agent can handle the `SameSite=None` parameter
16 * by mapping against known incompatible clients.
20 * // Get User Agent string.
21 * $rawUserAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
22 * $userAgent = mb_convert_encoding($rawUserAgent, 'UTF-8');
24 * // Get boolean representing User Agent compatibility.
25 * $shouldUseSameSite = CRM_Utils_SameSite::shouldSendSameSiteNone($userAgent);
27 * Based on code provided by "The Chromium Projects".
29 * @see https://www.chromium.org/updates/same-site/incompatible-clients
31 class CRM_Utils_SameSite
{
34 * Determine if the current User Agent can handle the `SameSite=None` parameter.
36 * @param str $userAgent The User Agent.
37 * @return bool True if the User Agent is compatible, FALSE otherwise.
39 public static function shouldSendSameSiteNone($userAgent) {
40 return !self
::isSameSiteNoneIncompatible($userAgent);
44 * Detect classes of browsers known to be incompatible.
46 * @param str $userAgent The User Agent.
47 * @return bool True if the User Agent is determined to be incompatible, FALSE otherwise.
49 private static function isSameSiteNoneIncompatible($userAgent) {
50 return self
::hasWebKitSameSiteBug($userAgent) ||
51 self
::dropsUnrecognizedSameSiteCookies($userAgent);
55 * Detect versions of Safari and embedded browsers on MacOS 10.14 and all
58 * These versions will erroneously treat cookies marked with `SameSite=None`
59 * as if they were marked `SameSite=Strict`.
61 * @param str $userAgent The User Agent.
64 private static function hasWebKitSameSiteBug($userAgent) {
65 return self
::isIosVersion(12, $userAgent) ||
(self
::isMacosxVersion(10, 14, $userAgent) &&
66 (self
::isSafari($userAgent) || self
::isMacEmbeddedBrowser($userAgent)));
70 * Detect versions of UC Browser on Android prior to version 12.13.2.
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.
77 * @param str $userAgent The User Agent.
80 private static function dropsUnrecognizedSameSiteCookies($userAgent) {
81 if (self
::isUcBrowser($userAgent)) {
82 return !self
::isUcBrowserVersionAtLeast(12, 13, 2, $userAgent);
85 return self
::isChromiumBased($userAgent) &&
86 self
::isChromiumVersionAtLeast(51, $userAgent, '>=') &&
87 self
::isChromiumVersionAtLeast(67, $userAgent, '<=');
93 * @param int $major The major version to test.
94 * @param str $userAgent The User Agent.
97 private static function isIosVersion($major, $userAgent) {
98 $regex = "/\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\//";
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, '<=');
111 * Detect MacOS version.
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.
118 private static function isMacosxVersion($major, $minor, $userAgent) {
119 $regex = "/\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\//";
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, '<=');
132 * Detect MacOS Safari.
134 * @param str $userAgent The User Agent.
137 private static function isSafari($userAgent) {
138 $regex = "/Version\/.* Safari\//";
139 return preg_match($regex, $userAgent) && !self
::isChromiumBased($userAgent);
143 * Detect MacOS embedded browser.
145 * @param str $userAgent The User Agent.
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);
154 * Detect if browser is Chromium based.
156 * @param str $userAgent The User Agent.
159 private static function isChromiumBased($userAgent) {
160 $regex = "/Chrom(e|ium)/";
161 return preg_match($regex, $userAgent);
165 * Detect if Chromium version meets requirements.
167 * @param int $major The major version to test.
168 * @param str $userAgent The User Agent.
169 * @param str $operator
172 private static function isChromiumVersionAtLeast($major, $userAgent, $operator) {
173 $regex = "/Chrom[^ \/]+\/(\d+)[\.\d]* /";
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);
187 * @param str $userAgent The User Agent.
190 private static function isUcBrowser($userAgent) {
191 $regex = "/UCBrowser\//";
192 return preg_match($regex, $userAgent);
196 * Detect if UCBrowser version meets requirements.
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.
204 private static function isUcBrowserVersionAtLeast($major, $minor, $build, $userAgent) {
205 $regex = "/UCBrowser\/(\d+)\.(\d+)\.(\d+)[\.\d]* /";
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];
214 if (version_compare($majorVersion, $major, '>=')) {
215 if (version_compare($minorVersion, $minor, '>=')) {
216 return version_compare($buildVersion, $build, '>=');