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)