Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useragent-based detection of JS version #10776

Merged
merged 4 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions homeassistant/components/frontend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from homeassistant.core import callback
from homeassistant.loader import bind_hass

REQUIREMENTS = ['home-assistant-frontend==20171121.1']
REQUIREMENTS = ['home-assistant-frontend==20171121.1', 'user-agents==1.1.0']

DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
Expand All @@ -32,9 +32,10 @@

CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5'
JS_DEFAULT_OPTION = 'auto'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should keep this on es5 for one more release and have people be able to try out auto.

JS_OPTIONS = ['es5', 'latest', 'auto']

DEFAULT_THEME_COLOR = '#03A9F4'
Expand Down Expand Up @@ -63,6 +64,7 @@
DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5'
DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme'
DEFAULT_THEME = 'default'
Expand All @@ -79,6 +81,8 @@
}),
vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXTRA_HTML_URL_ES5):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS)
}),
Expand Down Expand Up @@ -269,11 +273,12 @@ def async_register_panel(hass, component_name, path, md5=None,

@bind_hass
@callback
def add_extra_html_url(hass, url):
def add_extra_html_url(hass, url, es5=False):
"""Register extra html url to load."""
url_set = hass.data.get(DATA_EXTRA_HTML_URL)
key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL
url_set = hass.data.get(key)
if url_set is None:
url_set = hass.data[DATA_EXTRA_HTML_URL] = set()
url_set = hass.data[key] = set()
url_set.add(url)


Expand Down Expand Up @@ -358,9 +363,13 @@ def finalize_panel(panel):

if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
if DATA_EXTRA_HTML_URL_ES5 not in hass.data:
hass.data[DATA_EXTRA_HTML_URL_ES5] = set()

for url in conf.get(CONF_EXTRA_HTML_URL, []):
add_extra_html_url(hass, url)
add_extra_html_url(hass, url, False)
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
add_extra_html_url(hass, url, True)

yield from async_setup_themes(hass, conf.get(CONF_THEMES))

Expand Down Expand Up @@ -488,12 +497,14 @@ def get(self, request, extra=None):

template = yield from hass.async_add_job(self.get_template, latest)

extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5

resp = template.render(
no_auth=no_auth,
panel_url=panel_url,
panels=hass.data[DATA_PANELS],
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
extra_urls=hass.data[extra_key],
)

return web.Response(text=resp, content_type='text/html')
Expand Down Expand Up @@ -545,10 +556,36 @@ def _is_latest(js_option, request):
"""
if request is None:
return js_option == 'latest'
latest_in_query = 'latest' in request.query or (
request.headers.get('Referer') and
'latest' in urlparse(request.headers['Referer']).query)
es5_in_query = 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query)
return latest_in_query or (not es5_in_query and js_option == 'latest')

# latest in query
if 'latest' in request.query or (
request.headers.get('Referer') and
'latest' in urlparse(request.headers['Referer']).query):
return True

# es5 in query
if 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query):
return False

# non-auto option in config
if js_option != 'auto':
return js_option == 'latest'

from user_agents import parse
useragent = parse(request.headers.get('User-Agent'))

# on iOS every browser is a Safari which we support from version 10.
if useragent.os.family == 'iOS':
return useragent.os.version[0] >= 10

family_min_version = {
'Chrome': 50, # Probably can reduce this
'Firefox': 41, # Destructuring added in 41
'Opera': 40, # Probably can reduce this
'Edge': 14, # Maybe can reduce this
'Safari': 10, # many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,9 @@ uber_rides==0.6.0
# homeassistant.components.sensor.ups
upsmychoice==1.0.6

# homeassistant.components.frontend
user-agents==1.1.0

# homeassistant.components.camera.uvc
uvcclient==0.10.1

Expand Down
18 changes: 15 additions & 3 deletions tests/components/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from homeassistant.setup import async_setup_component
from homeassistant.components.frontend import (
DOMAIN, CONF_THEMES, CONF_EXTRA_HTML_URL, DATA_PANELS)
DOMAIN, CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5,
DATA_PANELS)


@pytest.fixture
Expand Down Expand Up @@ -36,7 +37,9 @@ def mock_http_client_with_urls(hass, test_client):
"""Start the Hass HTTP component."""
hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
DOMAIN: {
CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"]
CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"],
CONF_EXTRA_HTML_URL_ES5:
["https://domain.com/my_extra_url_es5.html"]
}}))
return hass.loop.run_until_complete(test_client(hass.http.app))

Expand Down Expand Up @@ -163,12 +166,21 @@ def test_missing_themes(mock_http_client):
@asyncio.coroutine
def test_extra_urls(mock_http_client_with_urls):
"""Test that extra urls are loaded."""
resp = yield from mock_http_client_with_urls.get('/states')
resp = yield from mock_http_client_with_urls.get('/states?latest')
assert resp.status == 200
text = yield from resp.text()
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0


@asyncio.coroutine
def test_extra_urls_es5(mock_http_client_with_urls):
"""Test that es5 extra urls are loaded."""
resp = yield from mock_http_client_with_urls.get('/states?es5')
assert resp.status == 200
text = yield from resp.text()
assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0


@asyncio.coroutine
def test_panel_without_path(hass):
"""Test panel registration without file path."""
Expand Down