Skip to content

Commit

Permalink
fix: browser parsing (#20657)
Browse files Browse the repository at this point in the history
* fix: browser parsing

Added new opera userAgent string.
Better logging for failures.
Better matching for version string.
Fixed android mistaken check.

Part of #20610

* Add ios firefox parsing

Add isIPad
Add testing userAgent strings
from json file.
  • Loading branch information
caalador authored Dec 12, 2024
1 parent c7a0714 commit 17295f7
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.io.Serializable;
import java.util.Locale;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.shared.BrowserDetails;

/**
Expand Down Expand Up @@ -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);
}
};
}
}

Expand Down
109 changes: 77 additions & 32 deletions flow-server/src/main/java/com/vaadin/flow/shared/BrowserDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,19 @@ 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");
isFirefox = userAgent.contains(" firefox/");
isSafari = !isChrome && !isIE && !isOpera
&& userAgent.contains("safari");
isFirefox = userAgent.contains(" firefox/")
|| userAgent.contains("fxios/");
if (userAgent.contains(" edge/") || userAgent.contains(" edg/")
|| userAgent.contains(" edga/")
|| userAgent.contains(" edgios/")) {
Expand Down Expand Up @@ -148,7 +151,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
Expand All @@ -161,18 +164,30 @@ 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));
int i = userAgent.indexOf(" fxios/");
if (i != -1) {
// Version present in Opera 10 and newer
i = userAgent.indexOf(" fxios/") + 7;
} else {
i = userAgent.indexOf(" firefox/") + 9;
}
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) {
Expand Down Expand Up @@ -206,10 +221,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/")) {
Expand All @@ -220,7 +240,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
Expand Down Expand Up @@ -274,16 +297,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);
}
}

Expand All @@ -298,11 +321,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);
}
}

Expand All @@ -327,7 +352,7 @@ private static int getVersionStringLength(String userAgent,

private void parseAndroidVersion(String userAgent) {
// Android 5.1;
if (!userAgent.contains("android")) {
if (!userAgent.contains("android ")) {
return;
}

Expand All @@ -337,7 +362,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) {
Expand All @@ -349,35 +374,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) {
Expand All @@ -390,7 +423,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,
Expand All @@ -410,11 +444,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;
}
Expand Down Expand Up @@ -598,6 +634,15 @@ public boolean isIPhone() {
return isIPhone;
}

/**
* Tests if the browser is run on iPad.
*
* @return true if run on iPad, false otherwise
*/
public boolean isIPad() {
return isIPad;
}

/**
* Tests if the browser is run on Chrome OS (e.g. a Chromebook).
*
Expand Down Expand Up @@ -667,10 +712,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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit 17295f7

Please # to comment.