SP/web2py/gluon/contrib/user_agent_parser.py
Saturneic 064f602b1a Add.
2018-10-25 23:33:13 +08:00

696 lines
17 KiB
Python

"""
Extract client information from http user agent
The module does not try to detect all capabilities of browser in current form (it can easily be extended though).
Tries to
* be fast
* very easy to extend
* reliable enough for practical purposes
* assist python web apps to detect clients.
"""
__version__ = '1.7.8'
class DetectorsHub(dict):
_known_types = ['os', 'dist', 'flavor', 'browser']
def __init__(self, *args, **kw):
dict.__init__(self, *args, **kw)
for typ in self._known_types:
self.setdefault(typ, [])
self.registerDetectors()
def register(self, detector):
if detector.info_type not in self._known_types:
self[detector.info_type] = [detector]
self._known_types.insert(detector.order, detector.info_type)
else:
self[detector.info_type].append(detector)
def __iter__(self):
return iter(self._known_types)
def registerDetectors(self):
detectors = [v() for v in globals().values() if DetectorBase in getattr(v, '__mro__', [])]
for d in detectors:
if d.can_register:
self.register(d)
class DetectorBase(object):
name = "" # "to perform match in DetectorsHub object"
info_type = "override me"
result_key = "override me"
order = 10 # 0 is highest
look_for = "string to look for"
skip_if_found = [] # strings if present stop processin
can_register = False
version_markers = [("/", " ")]
allow_space_in_version = False
_suggested_detectors = None
platform = None
bot = False
def __init__(self):
if not self.name:
self.name = self.__class__.__name__
self.can_register = (self.__class__.__dict__.get('can_register', True))
def detect(self, agent, result):
# -> True/None
word = self.checkWords(agent)
if word:
result[self.info_type] = dict(name=self.name)
result['bot'] = self.bot
version = self.getVersion(agent, word)
if version:
result[self.info_type]['version'] = version
if self.platform:
result['platform'] = {'name': self.platform, 'version': version}
return True
def checkWords(self, agent):
# -> True/None
for w in self.skip_if_found:
if w in agent:
return False
if isinstance(self.look_for, (tuple, list)):
for word in self.look_for:
if word in agent:
return word
elif self.look_for in agent:
return self.look_for
def getVersion(self, agent, word):
"""
=> version string /None
"""
version_markers = self.version_markers if \
isinstance(self.version_markers[0], (list, tuple)) else [self.version_markers]
version_part = agent.split(word, 1)[-1]
for start, end in version_markers:
if version_part.startswith(start) and end in version_part:
version = version_part[1:]
if end: # end could be empty string
version = version.split(end)[0]
if not self.allow_space_in_version:
version = version.split()[0]
return version
class OS(DetectorBase):
info_type = "os"
can_register = False
version_markers = [";", " "]
allow_space_in_version = True
platform = None
class Dist(DetectorBase):
info_type = "dist"
can_register = False
platform = None
class Flavor(DetectorBase):
info_type = "flavor"
can_register = False
platform = None
class Browser(DetectorBase):
info_type = "browser"
can_register = False
class Firefox(Browser):
look_for = "Firefox"
version_markers = [('/', '')]
skip_if_found = ["SeaMonkey", "web/snippet"]
class SeaMonkey(Browser):
look_for = "SeaMonkey"
version_markers = [('/', '')]
class Konqueror(Browser):
look_for = "Konqueror"
version_markers = ["/", ";"]
class OperaMobile(Browser):
look_for = "Opera Mobi"
name = "Opera Mobile"
def getVersion(self, agent, word):
try:
look_for = "Version"
return agent.split(look_for)[1][1:].split(' ')[0]
except IndexError:
look_for = "Opera"
return agent.split(look_for)[1][1:].split(' ')[0]
class Opera(Browser):
look_for = "Opera"
def getVersion(self, agent, word):
try:
look_for = "Version"
return agent.split(look_for)[1][1:].split(' ')[0]
except IndexError:
look_for = "Opera"
version = agent.split(look_for)[1][1:].split(' ')[0]
return version.split('(')[0]
class OperaNew(Browser):
"""
Opera after version 15
"""
name = "Opera"
look_for = "OPR"
version_markers = [('/', '')]
class Netscape(Browser):
look_for = "Netscape"
version_markers = [("/", '')]
class Trident(Browser):
look_for = "Trident"
skip_if_found = ["MSIE", "Opera"]
name = "Microsoft Internet Explorer"
version_markers = ["/", ";"]
trident_to_ie_versions = {
'4.0': '8.0',
'5.0': '9.0',
'6.0': '10.0',
'7.0': '11.0',
}
def getVersion(self, agent, word):
return self.trident_to_ie_versions.get(super(Trident, self).getVersion(agent, word))
class MSIE(Browser):
look_for = "MSIE"
skip_if_found = ["Opera"]
name = "Microsoft Internet Explorer"
version_markers = [" ", ";"]
class MSEdge(Browser):
look_for = "Edge"
skip_if_found = ["MSIE"]
version_markers = ["/", ""]
class Galeon(Browser):
look_for = "Galeon"
class WOSBrowser(Browser):
look_for = "wOSBrowser"
def getVersion(self, agent, word):
pass
class Safari(Browser):
look_for = "Safari"
skip_if_found = ["Edge"]
def checkWords(self, agent):
unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android"]
if self.look_for in agent:
for word in unless_list:
if word in agent:
return False
return self.look_for
def getVersion(self, agent, word):
if "Version/" in agent:
return agent.split('Version/')[-1].split(' ')[0].strip()
if "Safari/" in agent:
return agent.split('Safari/')[-1].split(' ')[0].strip()
else:
return agent.split('Safari ')[-1].split(' ')[0].strip() # Mobile Safari
class GoogleBot(Browser):
# https://support.google.com/webmasters/answer/1061943
look_for = ["Googlebot", "Googlebot-News", "Googlebot-Image",
"Googlebot-Video", "Googlebot-Mobile", "Mediapartners-Google",
"Mediapartners", "AdsBot-Google", "web/snippet"]
bot = True
version_markers = [('/', ';'), ('/', ' ')]
class GoogleFeedFetcher(Browser):
look_for = "Feedfetcher-Google"
bot = True
def get_version(self, agent):
pass
class RunscopeRadar(Browser):
look_for = "runscope-radar"
bot = True
class GoogleAppEngine(Browser):
look_for = "AppEngine-Google"
bot = True
def get_version(self, agent):
pass
class GoogleApps(Browser):
look_for = "GoogleApps script"
bot = True
def get_version(self, agent):
pass
class TwitterBot(Browser):
look_for = "Twitterbot"
bot = True
class MJ12Bot(Browser):
look_for = "MJ12bot"
bot = True
class YandexBot(Browser):
# http://help.yandex.com/search/robots/agent.xml
look_for = "Yandex"
bot = True
def getVersion(self, agent, word):
return agent[agent.index('Yandex'):].split('/')[-1].split(')')[0].strip()
class BingBot(Browser):
look_for = "bingbot"
version_markers = ["/", ";"]
bot = True
class BaiduBot(Browser):
# http://help.baidu.com/question?prod_en=master&class=1&id=1000973
look_for = ["Baiduspider", "Baiduspider-image", "Baiduspider-video",
"Baiduspider-news", "Baiduspider-favo", "Baiduspider-cpro",
"Baiduspider-ads"]
bot = True
version_markers = ('/', ';')
class LinkedInBot(Browser):
look_for = "LinkedInBot"
bot = True
class ArchiveDotOrgBot(Browser):
look_for = "archive.org_bot"
bot = True
class YoudaoBot(Browser):
look_for = "YoudaoBot"
bot = True
class YoudaoBotImage(Browser):
look_for = "YodaoBot-Image"
bot = True
class RogerBot(Browser):
look_for = "rogerbot"
bot = True
class TweetmemeBot(Browser):
look_for = "TweetmemeBot"
bot = True
class WebshotBot(Browser):
look_for = "WebshotBot"
bot = True
class SensikaBot(Browser):
look_for = "SensikaBot"
bot = True
class YesupBot(Browser):
look_for = "YesupBot"
bot = True
class DotBot(Browser):
look_for = "DotBot"
bot = True
class PhantomJS(Browser):
look_for = "Browser/Phantom"
bot = True
class FacebookExternalHit(Browser):
look_for = 'facebookexternalhit'
bot = True
class NokiaOvi(Browser):
look_for = "S40OviBrowser"
class UCBrowser(Browser):
look_for = "UCBrowser"
class BrowserNG(Browser):
look_for = "BrowserNG"
class Dolfin(Browser):
look_for = 'Dolfin'
class NetFront(Browser):
look_for = 'NetFront'
class Jasmine(Browser):
look_for = 'Jasmine'
class Openwave(Browser):
look_for = 'Openwave'
class UPBrowser(Browser):
look_for = 'UP.Browser'
class OneBrowser(Browser):
look_for = 'OneBrowser'
class ObigoInternetBrowser(Browser):
look_for = 'ObigoInternetBrowser'
class TelecaBrowser(Browser):
look_for = 'TelecaBrowser'
class MAUI(Browser):
look_for = 'Browser/MAUI'
def getVersion(self, agent, word):
version = agent.split("Release/")[-1][:10]
return version
class NintendoBrowser(Browser):
look_for = 'NintendoBrowser'
class AndroidBrowser(Browser):
look_for = "Android"
skip_if_found = ['Chrome', 'Windows Phone']
# http://decadecity.net/blog/2013/11/21/android-browser-versions
def getVersion(self, agent, word):
pass
class Linux(OS):
look_for = 'Linux'
platform = 'Linux'
def getVersion(self, agent, word):
pass
class Blackberry(OS):
look_for = 'BlackBerry'
platform = 'BlackBerry'
def getVersion(self, agent, word):
pass
class BlackberryPlaybook(Dist):
look_for = 'PlayBook'
platform = 'BlackBerry'
def getVersion(self, agent, word):
pass
class WindowsPhone(OS):
name = "Windows Phone"
platform = 'Windows'
look_for = ["Windows Phone OS", "Windows Phone"]
version_markers = [(" ", ";"), (" ", ")")]
class iOS(OS):
look_for = ('iPhone', 'iPad')
skip_if_found = ['like iPhone']
class iPhone(Dist):
look_for = 'iPhone'
platform = 'iOS'
skip_if_found = ['like iPhone']
def getVersion(self, agent, word):
version_end_chars = [' ']
if not "iPhone OS" in agent:
return None
part = agent.split('iPhone OS')[-1].strip()
for c in version_end_chars:
if c in part:
version = part.split(c)[0]
return version.replace('_', '.')
return None
class IPad(Dist):
look_for = 'iPad;'
platform = 'iOS'
def getVersion(self, agent, word):
version_end_chars = [' ']
if not "CPU OS " in agent:
return None
part = agent.split('CPU OS ')[-1].strip()
for c in version_end_chars:
if c in part:
version = part.split(c)[0]
return version.replace('_', '.')
return None
class Macintosh(OS):
look_for = 'Macintosh'
def getVersion(self, agent, word):
pass
class MacOS(Flavor):
look_for = 'Mac OS'
platform = 'Mac OS'
skip_if_found = ['iPhone', 'iPad']
def getVersion(self, agent, word):
version_end_chars = [';', ')']
part = agent.split('Mac OS')[-1].strip()
for c in version_end_chars:
if c in part:
version = part.split(c)[0]
return version.replace('_', '.')
return ''
class Windows(Dist):
look_for = 'Windows'
platform = 'Windows'
class Windows(OS):
look_for = 'Windows'
platform = 'Windows'
skip_if_found = ["Windows Phone"]
win_versions = {
"NT 10.0": "10",
"NT 6.3": "8.1",
"NT 6.2": "8",
"NT 6.1": "7",
"NT 6.0": "Vista",
"NT 5.2": "Server 2003 / XP x64",
"NT 5.1": "XP",
"NT 5.01": "2000 SP1",
"NT 5.0": "2000",
"98; Win 9x 4.90": "Me"
}
def getVersion(self, agent, word):
v = agent.split('Windows')[-1].split(';')[0].strip()
if ')' in v:
v = v.split(')')[0]
v = self.win_versions.get(v, v)
return v
class Ubuntu(Dist):
look_for = 'Ubuntu'
version_markers = ["/", " "]
class Debian(Dist):
look_for = 'Debian'
version_markers = ["/", " "]
class Chrome(Browser):
look_for = "Chrome"
version_markers = ["/", " "]
skip_if_found = ["OPR", "Edge"]
def getVersion(self, agent, word):
part = agent.split(word + self.version_markers[0])[-1]
version = part.split(self.version_markers[1])[0]
if '+' in version:
version = part.split('+')[0]
return version.strip()
class ChromeiOS(Browser):
look_for = "CriOS"
version_markers = ["/", " "]
class ChromeOS(OS):
look_for = "CrOS"
platform = ' ChromeOS'
version_markers = [" ", " "]
def getVersion(self, agent, word):
version_markers = self.version_markers
if word + '+' in agent:
version_markers = ['+', '+']
return agent.split(word + version_markers[0])[-1].split(version_markers[1])[1].strip()[:-1]
class Android(Dist):
look_for = 'Android'
platform = 'Android'
skip_if_found = ['Windows Phone']
def getVersion(self, agent, word):
return agent.split(word)[-1].split(';')[0].strip()
class WebOS(Dist):
look_for = 'hpwOS'
def getVersion(self, agent, word):
return agent.split('hpwOS/')[-1].split(';')[0].strip()
class NokiaS40(OS):
look_for = 'Series40'
platform = 'Nokia S40'
def getVersion(self, agent, word):
pass
class Symbian(OS):
look_for = ['Symbian', 'SymbianOS']
platform = 'Symbian'
class PlayStation(OS):
look_for = ['PlayStation', 'PLAYSTATION']
platform = 'PlayStation'
version_markers = [" ", ")"]
class prefs: # experimental
os = dict(
Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])),
BlackBerry=dict(dist=[BlackberryPlaybook]),
Macintosh=dict(flavor=[MacOS]),
Windows=dict(browser=[MSIE, Firefox]),
ChromeOS=dict(browser=[Chrome]),
Debian=dict(browser=[Firefox])
)
dist = dict(
Ubuntu=dict(browser=[Firefox]),
Android=dict(browser=[Safari]),
IPhone=dict(browser=[Safari]),
IPad=dict(browser=[Safari]),
)
flavor = dict(
MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE])
)
detectorshub = DetectorsHub()
def detect(agent, fill_none=False):
"""
fill_none: if name/version is not detected respective key is still added to the result with value None
"""
result = dict(platform=dict(name=None, version=None))
_suggested_detectors = []
for info_type in detectorshub:
detectors = _suggested_detectors or detectorshub[info_type]
for detector in detectors:
try:
detector.detect(agent, result)
except Exception as _err:
pass
if fill_none:
attrs_d = {'name': None, 'version': None}
for key in ('os', 'browser'):
if key not in result:
result[key] = attrs_d
else:
for k, v in attrs_d.items():
result[k] = v
return result
def simple_detect(agent):
"""
-> (os, browser) # tuple of strings
"""
result = detect(agent)
os_list = []
if 'flavor' in result:
os_list.append(result['flavor']['name'])
if 'dist' in result:
os_list.append(result['dist']['name'])
if 'os' in result:
os_list.append(result['os']['name'])
os = os_list and " ".join(os_list) or "Unknown OS"
os_version = os_list and (result.get('flavor') and result['flavor'].get('version')) or \
(result.get('dist') and result['dist'].get('version')) or (result.get('os') and result['os'].get('version')) or ""
browser = 'browser' in result and result['browser'].get('name') or 'Unknown Browser'
browser_version = 'browser' in result and result['browser'].get('version') or ""
if browser_version:
browser = " ".join((browser, browser_version))
if os_version:
os = " ".join((os, os_version))
return os, browser
class mobilize(object):
"""
Decorator for controller functions so they use different views for mobile devices.
WARNING: If you update httpagentparser make sure to leave mobilize for
backwards compatibility.
"""
def __init__(self, func):
self.func = func
def __call__(self):
from gluon import current
user_agent = current.request.user_agent()
if user_agent.is_mobile:
items = current.response.view.split('.')
items.insert(-1, 'mobile')
current.response.view = '.'.join(items)
return self.func()