Add useragent-based detection of JS version (#10776)

* Add useragent-based detection of JS version

* Keep es5 as default meanwhile

* Update test
This commit is contained in:
Andrey 2017-11-29 08:53:12 +02:00 committed by Paulus Schoutsen
parent 7ab15c0e79
commit 99ea2c17a1
3 changed files with 69 additions and 16 deletions

View File

@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171127.0'] REQUIREMENTS = ['home-assistant-frontend==20171127.0', 'user-agents==1.1.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -32,6 +32,7 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
CONF_THEMES = 'themes' CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo' CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version' CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5' JS_DEFAULT_OPTION = 'es5'
@ -63,6 +64,7 @@ DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels' DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version' DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5'
DATA_THEMES = 'frontend_themes' DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme' DATA_DEFAULT_THEME = 'frontend_default_theme'
DEFAULT_THEME = 'default' DEFAULT_THEME = 'default'
@ -79,6 +81,8 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
vol.Optional(CONF_EXTRA_HTML_URL): vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]), 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.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS) vol.In(JS_OPTIONS)
}), }),
@ -269,11 +273,12 @@ def async_register_panel(hass, component_name, path, md5=None,
@bind_hass @bind_hass
@callback @callback
def add_extra_html_url(hass, url): def add_extra_html_url(hass, url, es5=False):
"""Register extra html url to load.""" """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: if url_set is None:
url_set = hass.data[DATA_EXTRA_HTML_URL] = set() url_set = hass.data[key] = set()
url_set.add(url) url_set.add(url)
@ -358,9 +363,13 @@ def async_setup(hass, config):
if DATA_EXTRA_HTML_URL not in hass.data: if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set() 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, []): 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)) yield from async_setup_themes(hass, conf.get(CONF_THEMES))
@ -488,12 +497,14 @@ class IndexView(HomeAssistantView):
template = yield from hass.async_add_job(self.get_template, latest) 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( resp = template.render(
no_auth=no_auth, no_auth=no_auth,
panel_url=panel_url, panel_url=panel_url,
panels=hass.data[DATA_PANELS], panels=hass.data[DATA_PANELS],
theme_color=MANIFEST_JSON['theme_color'], 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') return web.Response(text=resp, content_type='text/html')
@ -545,10 +556,36 @@ def _is_latest(js_option, request):
""" """
if request is None: if request is None:
return js_option == 'latest' return js_option == 'latest'
latest_in_query = 'latest' in request.query or (
request.headers.get('Referer') and # latest in query
'latest' in urlparse(request.headers['Referer']).query) if 'latest' in request.query or (
es5_in_query = 'es5' in request.query or ( request.headers.get('Referer') and
request.headers.get('Referer') and 'latest' in urlparse(request.headers['Referer']).query):
'es5' in urlparse(request.headers['Referer']).query) return True
return latest_in_query or (not es5_in_query and js_option == 'latest')
# 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

View File

@ -1100,6 +1100,9 @@ uber_rides==0.6.0
# homeassistant.components.sensor.ups # homeassistant.components.sensor.ups
upsmychoice==1.0.6 upsmychoice==1.0.6
# homeassistant.components.frontend
user-agents==1.1.0
# homeassistant.components.camera.uvc # homeassistant.components.camera.uvc
uvcclient==0.10.1 uvcclient==0.10.1

View File

@ -7,7 +7,8 @@ import pytest
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.components.frontend import ( from homeassistant.components.frontend import (
DOMAIN, CONF_THEMES, CONF_EXTRA_HTML_URL, DATA_PANELS) DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
CONF_EXTRA_HTML_URL_ES5, DATA_PANELS)
@pytest.fixture @pytest.fixture
@ -36,7 +37,10 @@ def mock_http_client_with_urls(hass, test_client):
"""Start the Hass HTTP component.""" """Start the Hass HTTP component."""
hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
DOMAIN: { DOMAIN: {
CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"] CONF_JS_VERSION: 'auto',
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)) return hass.loop.run_until_complete(test_client(hass.http.app))
@ -163,12 +167,21 @@ def test_missing_themes(mock_http_client):
@asyncio.coroutine @asyncio.coroutine
def test_extra_urls(mock_http_client_with_urls): def test_extra_urls(mock_http_client_with_urls):
"""Test that extra urls are loaded.""" """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 assert resp.status == 200
text = yield from resp.text() text = yield from resp.text()
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 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 @asyncio.coroutine
def test_panel_without_path(hass): def test_panel_without_path(hass):
"""Test panel registration without file path.""" """Test panel registration without file path."""