You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
7.1 KiB

3 years ago
import re
import typing as t
import warnings
from .user_agent import UserAgent as _BaseUserAgent
if t.TYPE_CHECKING:
from _typeshed.wsgi import WSGIEnvironment
class _UserAgentParser:
platform_rules: t.ClassVar[t.Iterable[t.Tuple[str, str]]] = (
(" cros ", "chromeos"),
("iphone|ios", "iphone"),
("ipad", "ipad"),
(r"darwin\b|mac\b|os\s*x", "macos"),
("win", "windows"),
(r"android", "android"),
("netbsd", "netbsd"),
("openbsd", "openbsd"),
("freebsd", "freebsd"),
("dragonfly", "dragonflybsd"),
("(sun|i86)os", "solaris"),
(r"x11\b|lin(\b|ux)?", "linux"),
(r"nintendo\s+wii", "wii"),
("irix", "irix"),
("hp-?ux", "hpux"),
("aix", "aix"),
("sco|unix_sv", "sco"),
("bsd", "bsd"),
("amiga", "amiga"),
("blackberry|playbook", "blackberry"),
("symbian", "symbian"),
)
browser_rules: t.ClassVar[t.Iterable[t.Tuple[str, str]]] = (
("googlebot", "google"),
("msnbot", "msn"),
("yahoo", "yahoo"),
("ask jeeves", "ask"),
(r"aol|america\s+online\s+browser", "aol"),
(r"opera|opr", "opera"),
("edge|edg", "edge"),
("chrome|crios", "chrome"),
("seamonkey", "seamonkey"),
("firefox|firebird|phoenix|iceweasel", "firefox"),
("galeon", "galeon"),
("safari|version", "safari"),
("webkit", "webkit"),
("camino", "camino"),
("konqueror", "konqueror"),
("k-meleon", "kmeleon"),
("netscape", "netscape"),
(r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"),
("lynx", "lynx"),
("links", "links"),
("Baiduspider", "baidu"),
("bingbot", "bing"),
("mozilla", "mozilla"),
)
_browser_version_re = r"(?:{pattern})[/\sa-z(]*(\d+[.\da-z]+)?"
_language_re = re.compile(
r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|"
r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)"
)
def __init__(self) -> None:
self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platform_rules]
self.browsers = [
(b, re.compile(self._browser_version_re.format(pattern=a), re.I))
for a, b in self.browser_rules
]
def __call__(
self, user_agent: str
) -> t.Tuple[t.Optional[str], t.Optional[str], t.Optional[str], t.Optional[str]]:
platform: t.Optional[str]
browser: t.Optional[str]
version: t.Optional[str]
language: t.Optional[str]
for platform, regex in self.platforms: # noqa: B007
match = regex.search(user_agent)
if match is not None:
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[last_closing_paren:])
if match is not None:
version = match.group(1)
break
else:
browser = version = None
match = self._language_re.search(user_agent)
if match is not None:
language = match.group(1) or match.group(2)
else:
language = None
return platform, browser, version, language
# It wasn't public, but users might have imported it anyway, show a
# warning if a user created an instance.
class UserAgentParser(_UserAgentParser):
"""A simple user agent parser. Used by the `UserAgent`.
.. deprecated:: 2.0
Will be removed in Werkzeug 2.1. Use a dedicated parser library
instead.
"""
def __init__(self) -> None:
warnings.warn(
"'UserAgentParser' is deprecated and will be removed in"
" Werkzeug 2.1. Use a dedicated parser library instead.",
DeprecationWarning,
stacklevel=2,
)
super().__init__()
class _deprecated_property(property):
def __init__(self, fget: t.Callable[["_UserAgent"], t.Any]) -> None:
super().__init__(fget)
self.message = (
"The built-in user agent parser is deprecated and will be"
f" removed in Werkzeug 2.1. The {fget.__name__!r} property"
" will be 'None'. Subclass 'werkzeug.user_agent.UserAgent'"
" and set 'Request.user_agent_class' to use a different"
" parser."
)
def __get__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
warnings.warn(self.message, DeprecationWarning, stacklevel=3)
return super().__get__(*args, **kwargs)
# This is what Request.user_agent returns for now, only show warnings on
# attribute access, not creation.
class _UserAgent(_BaseUserAgent):
_parser = _UserAgentParser()
def __init__(self, string: str) -> None:
super().__init__(string)
info = self._parser(string)
self._platform, self._browser, self._version, self._language = info
@_deprecated_property
def platform(self) -> t.Optional[str]: # type: ignore
return self._platform
@_deprecated_property
def browser(self) -> t.Optional[str]: # type: ignore
return self._browser
@_deprecated_property
def version(self) -> t.Optional[str]: # type: ignore
return self._version
@_deprecated_property
def language(self) -> t.Optional[str]: # type: ignore
return self._language
# This is what users might be importing, show warnings on create.
class UserAgent(_UserAgent):
"""Represents a parsed user agent header value.
This uses a basic parser to try to extract some information from the
header.
:param environ_or_string: The header value to parse, or a WSGI
environ containing the header.
.. deprecated:: 2.0
Will be removed in Werkzeug 2.1. Subclass
:class:`werkzeug.user_agent.UserAgent` (note the new module
name) to use a dedicated parser instead.
.. versionchanged:: 2.0
Passing a WSGI environ is deprecated and will be removed in 2.1.
"""
def __init__(self, environ_or_string: "t.Union[str, WSGIEnvironment]") -> None:
if isinstance(environ_or_string, dict):
warnings.warn(
"Passing an environ to 'UserAgent' is deprecated and"
" will be removed in Werkzeug 2.1. Pass the header"
" value string instead.",
DeprecationWarning,
stacklevel=2,
)
string = environ_or_string.get("HTTP_USER_AGENT", "")
else:
string = environ_or_string
warnings.warn(
"The 'werkzeug.useragents' module is deprecated and will be"
" removed in Werkzeug 2.1. The new base API is"
" 'werkzeug.user_agent.UserAgent'.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(string)