Repository: pallets/werkzeug

Issue:
User agents not being detected
While using your utility to parse user agents for calls to my site, I have a few that don't get parsed correctly:

Version not getting parsed on the following user agents:

* `Mozilla/5.0 (Linux; Android 10; motorola edge) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.101 Mobile Safari/537.36`
* `Mozilla/5.0 (Linux; Android 10; motorola edge) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.81 Mobile Safari/537.36`
* `Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322)`

Detected as 'mozilla' but is actually msie v11:

* `Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko`


Discussion:
See my comments on issue [#1923](https://github.com/pallets/werkzeug/issues/1923)

We should probably merge these 2 issues since they share the same root problem. 
Happy to consider a PR that updates the matching rules.
I'd like to work on this.

Resolving patch:
diff --git a/src/werkzeug/useragents.py b/src/werkzeug/useragents.py
--- a/src/werkzeug/useragents.py
+++ b/src/werkzeug/useragents.py
@@ -84,8 +84,18 @@ def __call__(
                 break
         else:
             platform = None
+
+        # Except for Trident, all browser key words come after the last ')'
+        last_closing_paren = 0
+        if (
+            not re.compile(r"trident/.+? rv:", re.I).search(user_agent)
+            and ")" in user_agent
+            and user_agent[-1] != ")"
+        ):
+            last_closing_paren = user_agent.rindex(")")
+
         for browser, regex in self.browsers:  # noqa: B007
-            match = regex.search(user_agent)
+            match = regex.search(user_agent[last_closing_paren:])
             if match is not None:
                 version = match.group(1)
                 break


Test patch:
diff --git a/tests/test_useragents.py b/tests/test_useragents.py
--- a/tests/test_useragents.py
+++ b/tests/test_useragents.py
@@ -30,7 +30,7 @@
             None,
         ),
         (
-            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4123.0 Safari/537.36 Edg/84.0.499.0 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",  # noqa B950
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4123.0 Safari/537.36 Edg/84.0.499.0",  # noqa B950
             "windows",
             "edge",
             "84.0.499.0",
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -446,8 +446,9 @@ def test_etag_request():
     assert request.if_unmodified_since == dt
 
 
-def test_user_agent():
-    user_agents = [
+@pytest.mark.parametrize(
+    ("user_agent", "browser", "platform", "version", "language"),
+    (
         (
             "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.11) "
             "Gecko/20071127 Firefox/2.0.0.11",
@@ -626,19 +627,38 @@ def test_user_agent():
             "51.0.2704.106",
             None,
         ),
-    ]
-    for ua, browser, platform, version, lang in user_agents:
-        request = wrappers.Request(
-            {"HTTP_USER_AGENT": ua, "SERVER_NAME": "eggs", "SERVER_PORT": "80"}
-        )
-        assert request.user_agent.browser == browser
-        assert request.user_agent.platform == platform
-        assert request.user_agent.version == version
-        assert request.user_agent.language == lang
-        assert bool(request.user_agent)
-        assert request.user_agent.to_header() == ua
-        assert str(request.user_agent) == ua
+        (
+            "Mozilla/5.0 (Linux; Android; motorola edge) AppleWebKit/537.36 "
+            "(KHTML, like Gecko) Chrome/85.0.4183.81 Mobile Safari/537.36",
+            "chrome",
+            "android",
+            "85.0.4183.81",
+            None,
+        ),
+        (
+            "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; "
+            ".NET CLR 1.1.4322)",
+            "msie",
+            "windows",
+            "7.0",
+            None,
+        ),
+    ),
+)
+def test_user_agent(user_agent, browser, platform, version, language):
+    request = wrappers.Request(
+        {"HTTP_USER_AGENT": user_agent, "SERVER_NAME": "eggs", "SERVER_PORT": "80"}
+    )
+    assert request.user_agent.browser == browser
+    assert request.user_agent.platform == platform
+    assert request.user_agent.version == version
+    assert request.user_agent.language == language
+    assert bool(request.user_agent)
+    assert request.user_agent.to_header() == user_agent
+    assert str(request.user_agent) == user_agent
+
 
+def test_invalid_user_agent():
     request = wrappers.Request(
         {"HTTP_USER_AGENT": "foo", "SERVER_NAME": "eggs", "SERVER_PORT": "80"}
     )