From 951709d9ae3c93f7cdb8e8dce4df89ea37a7d411 Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Tue, 10 Dec 2024 07:35:14 +0200 Subject: [PATCH] fix: browser parsing Added new opera userAgent string. Better logging for failures. Better matching for version string. Fixed android mistaken check. Part of #20610 --- .../com/vaadin/flow/server/WebBrowser.java | 10 ++- .../vaadin/flow/shared/BrowserDetails.java | 89 ++++++++++++------- .../vaadin/flow/server/WebBrowserTest.java | 53 +++++++++++ .../flow/shared/BrowserDetailsTest.java | 11 +++ 4 files changed, 132 insertions(+), 31 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/server/WebBrowser.java b/flow-server/src/main/java/com/vaadin/flow/server/WebBrowser.java index 9f7b1529ef2..6d655169c91 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/WebBrowser.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/WebBrowser.java @@ -19,6 +19,8 @@ import java.io.Serializable; import java.util.Locale; +import org.slf4j.LoggerFactory; + import com.vaadin.flow.shared.BrowserDetails; /** @@ -65,7 +67,13 @@ public class WebBrowser implements Serializable { if (agent != null) { browserApplication = agent; - browserDetails = new BrowserDetails(agent); + browserDetails = new BrowserDetails(agent) { + @Override + protected void log(String error, Exception e) { + LoggerFactory.getLogger(BrowserDetails.class).error(error, + e); + } + }; } } diff --git a/flow-server/src/main/java/com/vaadin/flow/shared/BrowserDetails.java b/flow-server/src/main/java/com/vaadin/flow/shared/BrowserDetails.java index 5fbcc9ad88b..4bdb4b7b535 100644 --- a/flow-server/src/main/java/com/vaadin/flow/shared/BrowserDetails.java +++ b/flow-server/src/main/java/com/vaadin/flow/shared/BrowserDetails.java @@ -82,15 +82,17 @@ public BrowserDetails(String userAgent) { isWebKit = !isTrident && userAgent.contains("applewebkit"); // browser name - isChrome = userAgent.contains(CHROME) || userAgent.contains(" crios/") - || userAgent.contains(HEADLESSCHROME); - isOpera = userAgent.contains("opera"); + isChrome = (userAgent.contains(CHROME) || userAgent.contains(" crios/") + || userAgent.contains(HEADLESSCHROME)) + && !userAgent.contains(" opr/"); + isOpera = userAgent.contains("opera") || userAgent.contains(" opr/"); isIE = userAgent.contains("msie") && !isOpera && !userAgent.contains("webtv"); // IE 11 no longer contains MSIE in the user agent isIE = isIE || isTrident; - isSafari = !isChrome && !isIE && userAgent.contains("safari"); + isSafari = !isChrome && !isIE && !isOpera + && userAgent.contains("safari"); isFirefox = userAgent.contains(" firefox/"); if (userAgent.contains(" edge/") || userAgent.contains(" edg/") || userAgent.contains(" edga/") @@ -148,7 +150,7 @@ public BrowserDetails(String userAgent) { if (rvPos >= 0) { String tmp = userAgent.substring(rvPos + 3); tmp = tmp.replaceFirst("(\\.[0-9]+).+", "$1"); - parseVersionString(tmp); + parseVersionString(tmp, userAgent); } } else if (isTrident) { // potentially IE 11 in compatibility mode @@ -161,18 +163,24 @@ public BrowserDetails(String userAgent) { .substring(userAgent.indexOf("msie ") + 5); ieVersionString = safeSubstring(ieVersionString, 0, ieVersionString.indexOf(';')); - parseVersionString(ieVersionString); + parseVersionString(ieVersionString, userAgent); } } else if (isFirefox) { int i = userAgent.indexOf(" firefox/") + 9; - parseVersionString(safeSubstring(userAgent, i, i + 5)); + parseVersionString( + safeSubstring(userAgent, i, + i + getVersionStringLength(userAgent, i)), + userAgent); } else if (isChrome) { parseChromeVersion(userAgent); } else if (isSafari) { int i = userAgent.indexOf(" version/"); if (i >= 0) { i += 9; - parseVersionString(safeSubstring(userAgent, i, i + 5)); + parseVersionString( + safeSubstring(userAgent, i, + i + getVersionStringLength(userAgent, i)), + userAgent); } else { int engineVersion = (int) (browserEngineVersion * 10); if (engineVersion >= 6010 && engineVersion < 6015) { @@ -206,10 +214,15 @@ public BrowserDetails(String userAgent) { if (i != -1) { // Version present in Opera 10 and newer i += 9; // " version/".length + } else if (userAgent.contains(" opr/")) { + i = userAgent.indexOf(" opr/") + 5; } else { i = userAgent.indexOf("opera/") + 6; } - parseVersionString(safeSubstring(userAgent, i, i + 5)); + parseVersionString( + safeSubstring(userAgent, i, + i + getVersionStringLength(userAgent, i)), + userAgent); } else if (isEdge) { int i = userAgent.indexOf(" edge/") + 6; if (userAgent.contains(" edg/")) { @@ -220,7 +233,10 @@ public BrowserDetails(String userAgent) { i = userAgent.indexOf(" edgios/") + 8; } - parseVersionString(safeSubstring(userAgent, i, i + 8)); + parseVersionString( + safeSubstring(userAgent, i, + i + getVersionStringLength(userAgent, i)), + userAgent); } } catch (Exception e) { // Browser version parsing failed @@ -274,16 +290,16 @@ private void parseChromeOSVersion(String userAgent) { } String osVersionString = userAgent.substring(cur + 1, end); String[] parts = osVersionString.split("\\."); - parseChromeOsVersionParts(parts); + parseChromeOsVersionParts(parts, userAgent); } - private void parseChromeOsVersionParts(String[] parts) { + private void parseChromeOsVersionParts(String[] parts, String userAgent) { osMajorVersion = -1; osMinorVersion = -1; if (parts.length > 2) { - osMajorVersion = parseVersionPart(parts[0], OS_MAJOR); - osMinorVersion = parseVersionPart(parts[1], OS_MINOR); + osMajorVersion = parseVersionPart(parts[0], OS_MAJOR, userAgent); + osMinorVersion = parseVersionPart(parts[1], OS_MINOR, userAgent); } } @@ -298,11 +314,13 @@ private void parseChromeVersion(String userAgent) { i += CHROME.length(); } int versionBreak = getVersionStringLength(userAgent, i); - parseVersionString(safeSubstring(userAgent, i, i + versionBreak)); + parseVersionString(safeSubstring(userAgent, i, i + versionBreak), + userAgent); } else { i += crios.length(); // move index to version string start int versionBreak = getVersionStringLength(userAgent, i); - parseVersionString(safeSubstring(userAgent, i, i + versionBreak)); + parseVersionString(safeSubstring(userAgent, i, i + versionBreak), + userAgent); } } @@ -327,7 +345,7 @@ private static int getVersionStringLength(String userAgent, private void parseAndroidVersion(String userAgent) { // Android 5.1; - if (!userAgent.contains("android")) { + if (!userAgent.contains("android ")) { return; } @@ -337,7 +355,7 @@ private void parseAndroidVersion(String userAgent) { osVersionString = safeSubstring(osVersionString, 0, osVersionString.indexOf(";")); String[] parts = osVersionString.split("\\."); - parseOsVersion(parts); + parseOsVersion(parts, userAgent); } private void parseIOSVersion(String userAgent) { @@ -349,35 +367,43 @@ private void parseIOSVersion(String userAgent) { String osVersionString = safeSubstring(userAgent, userAgent.indexOf("os ") + 3, userAgent.indexOf(" like mac")); String[] parts = osVersionString.split("_"); - parseOsVersion(parts); + parseOsVersion(parts, userAgent); } - private void parseOsVersion(String[] parts) { + private void parseOsVersion(String[] parts, String userAgent) { osMajorVersion = -1; osMinorVersion = -1; if (parts.length >= 1) { - osMajorVersion = parseVersionPart(parts[0], OS_MAJOR); + osMajorVersion = parseVersionPart(parts[0], OS_MAJOR, userAgent); } if (parts.length >= 2) { // Some Androids report version numbers as "2.1-update1" int dashIndex = parts[1].indexOf('-'); if (dashIndex > -1) { String dashlessVersion = parts[1].substring(0, dashIndex); - osMinorVersion = parseVersionPart(dashlessVersion, OS_MINOR); + osMinorVersion = parseVersionPart(dashlessVersion, OS_MINOR, + userAgent); } else { - osMinorVersion = parseVersionPart(parts[1], OS_MINOR); + osMinorVersion = parseVersionPart(parts[1], OS_MINOR, + userAgent); } } } - private void parseVersionString(String versionString) { + private void parseVersionString(String versionString, String userAgent) { int idx = versionString.indexOf('.'); if (idx < 0) { idx = versionString.length(); } String majorVersionPart = safeSubstring(versionString, 0, idx); - browserMajorVersion = parseVersionPart(majorVersionPart, BROWSER_MAJOR); + browserMajorVersion = parseVersionPart(majorVersionPart, BROWSER_MAJOR, + userAgent); + + if (browserMajorVersion == -1) { + // no need to scan for minor if major version scanning failed. + return; + } int idx2 = versionString.indexOf('.', idx + 1); if (idx2 < 0) { @@ -390,7 +416,8 @@ private void parseVersionString(String versionString) { } String minorVersionPart = safeSubstring(versionString, idx + 1, idx2) .replaceAll("[^0-9].*", ""); - browserMinorVersion = parseVersionPart(minorVersionPart, BROWSER_MINOR); + browserMinorVersion = parseVersionPart(minorVersionPart, BROWSER_MINOR, + userAgent); } private static String safeSubstring(String string, int beginIndex, @@ -410,11 +437,13 @@ private static String safeSubstring(String string, int beginIndex, return string.substring(trimmedStart, trimmedEnd); } - private int parseVersionPart(String versionString, String partName) { + private int parseVersionPart(String versionString, String partName, + String userAgent) { try { return Integer.parseInt(versionString); } catch (Exception e) { - log(partName + " version parsing failed for: " + versionString, e); + log(partName + " version parsing failed for: " + versionString + + "\nWith userAgent: " + userAgent, e); } return -1; } @@ -667,10 +696,10 @@ && getOperatingSystemMinorVersion() >= 7))) { return false; } - private static void log(String error, Exception e) { + protected void log(String error, Exception e) { // "Logs" to stdout so the problem can be found but does not prevent // using the app. As this class is shared, we do not use - // java.util.logging + // slf4j for logging as normal. System.err.println(error + ' ' + e.getMessage()); } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/WebBrowserTest.java b/flow-server/src/test/java/com/vaadin/flow/server/WebBrowserTest.java index 73682141236..55664d2824b 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/WebBrowserTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/WebBrowserTest.java @@ -15,8 +15,12 @@ */ package com.vaadin.flow.server; +import java.util.Locale; + +import jdk.jfr.ValueDescriptor; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; public class WebBrowserTest { @@ -56,4 +60,53 @@ public void isIPhone_noDetails_returnsFalse() { public void isChromeOS_noDetails_returnsFalse() { Assert.assertFalse(browser.isChromeOS()); } + + @Test + public void isSafariOnMac_userDetails_returnsTrue() { + VaadinRequest request = initRequest( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_6_2) AppleWebKit/611.3.10.1.5 (KHTML, like Gecko) Version/14.1.2 Safari/611.3.10.1.5"); + + browser = new WebBrowser(request); + Assert.assertTrue(browser.isSafari()); + Assert.assertTrue(browser.isMacOSX()); + } + + @Test + public void isChromeOnWindows_userDetails_returnsTrue() { + VaadinRequest request = initRequest( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"); + + browser = new WebBrowser(request); + Assert.assertTrue(browser.isChrome()); + Assert.assertTrue(browser.isWindows()); + } + + @Test + public void isOperaOnWindows_userDetails_returnsTrue() { + VaadinRequest request = initRequest( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0"); + + browser = new WebBrowser(request); + Assert.assertTrue(browser.isOpera()); + Assert.assertTrue(browser.isWindows()); + } + + @Test + public void isFirefoxOnAndroid_userDetails_returnsTrue() { + VaadinRequest request = initRequest( + "Mozilla/5.0 (Android; Tablet; rv:33.0) Gecko/33.0 Firefox/33.0"); + + browser = new WebBrowser(request); + Assert.assertTrue(browser.isFirefox()); + Assert.assertTrue(browser.isAndroid()); + } + + private static VaadinRequest initRequest(String userAgent) { + VaadinRequest request = Mockito.mock(VaadinRequest.class); + Mockito.when(request.getLocale()).thenReturn(Locale.ENGLISH); + Mockito.when(request.getRemoteAddr()).thenReturn("0.0.0.0"); + Mockito.when(request.isSecure()).thenReturn(false); + Mockito.when(request.getHeader("User-Agent")).thenReturn(userAgent); + return request; + } } diff --git a/flow-server/src/test/java/com/vaadin/flow/shared/BrowserDetailsTest.java b/flow-server/src/test/java/com/vaadin/flow/shared/BrowserDetailsTest.java index aaeeb6ec393..dc477b68ed1 100644 --- a/flow-server/src/test/java/com/vaadin/flow/shared/BrowserDetailsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/shared/BrowserDetailsTest.java @@ -52,6 +52,7 @@ public class BrowserDetailsTest extends TestCase { private static final String OPERA964_WINDOWS = "Opera/9.64(Windows NT 5.1; U; en) Presto/2.1.1"; private static final String OPERA1010_WINDOWS = "Opera/9.80 (Windows NT 5.1; U; en) Presto/2.2.15 Version/10.10"; private static final String OPERA1050_WINDOWS = "Opera/9.80 (Windows NT 5.1; U; en) Presto/2.5.22 Version/10.50"; + private static final String OPERA115_WINDOWS = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0"; private static final String CHROME3_MAC = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198 Safari/532.0"; private static final String CHROME4_WINDOWS = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.89 Safari/532.5"; @@ -690,7 +691,17 @@ public void testHeadlessChrome() { assertBrowserMinorVersion(bd, 0); assertEngineVersion(bd, 537.36f); assertLinux(bd); + } + public void testOpera65() { + String userAgent = OPERA115_WINDOWS; + BrowserDetails bd = new BrowserDetails(userAgent); + assertWebKit(bd); + assertOpera(bd); + assertBrowserMajorVersion(bd, 115); + assertBrowserMinorVersion(bd, 0); + assertEngineVersion(bd, 537.36f); + assertWindows(bd); } public void testIos11FacebookBrowser() {