diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8a692d6f272..a18ed6eb3d1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,9 +4,10 @@ import logging import os import pathlib -from aiohttp import web +from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol import jinja2 +from yarl import URL import homeassistant.helpers.config_validation as cv from homeassistant.components.http.view import HomeAssistantView @@ -50,7 +51,6 @@ for size in (192, 384, 512, 1024): 'type': 'image/png' }) -DATA_FINALIZE_PANEL = 'frontend_finalize_panel' DATA_PANELS = 'frontend_panels' DATA_JS_VERSION = 'frontend_js_version' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' @@ -97,28 +97,6 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) -def generate_negative_index_regex(): - """Generate regex for index.""" - skip = [ - # files - "service_worker.js", - "robots.txt", - "onboarding.html", - "manifest.json", - ] - for folder in ( - "static", - "frontend_latest", - "frontend_es5", - "local", - "auth", - "api", - ): - # Regex matching static, static/, static/index.html - skip.append("{}(/|/.+|)".format(folder)) - return r"(?!(" + "|".join(skip) + r")).*" - - class Panel: """Abstract class for panels.""" @@ -256,7 +234,7 @@ async def async_setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - hass.http.register_view(IndexView(repo_path)) + hass.http.app.router.register_resource(IndexView(repo_path, hass)) for panel in ('kiosk', 'states', 'profile'): async_register_built_in_panel(hass, panel) @@ -327,21 +305,64 @@ def _async_setup_themes(hass, themes): hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) -class IndexView(HomeAssistantView): +class IndexView(web_urldispatcher.AbstractResource): """Serve the frontend.""" - url = '/' - name = 'frontend:index' - requires_auth = False - extra_urls = [ - "/{extra:%s}" % generate_negative_index_regex() - ] - - def __init__(self, repo_path): + def __init__(self, repo_path, hass): """Initialize the frontend view.""" + super().__init__(name="frontend:index") self.repo_path = repo_path + self.hass = hass self._template_cache = None + @property + def canonical(self) -> str: + """Return resource's canonical path.""" + return '/' + + @property + def _route(self): + """Return the index route.""" + return web_urldispatcher.ResourceRoute('GET', self.get, self) + + def url_for(self, **kwargs: str) -> URL: + """Construct url for resource with additional params.""" + return URL("/") + + async def resolve(self, request: web.Request): + """Resolve resource. + + Return (UrlMappingMatchInfo, allowed_methods) pair. + """ + if (request.path != '/' and + request.url.parts[1] not in self.hass.data[DATA_PANELS]): + return None, set() + + if request.method != hdrs.METH_GET: + return None, {'GET'} + + return web_urldispatcher.UrlMappingMatchInfo({}, self._route), {'GET'} + + def add_prefix(self, prefix: str) -> None: + """Add a prefix to processed URLs. + + Required for subapplications support. + """ + + def get_info(self): + """Return a dict with additional info useful for introspection.""" + return { + 'panels': list(self.hass.data[DATA_PANELS]) + } + + def freeze(self) -> None: + """Freeze the resource.""" + pass + + def raw_match(self, path: str) -> bool: + """Perform a raw match against path.""" + pass + def get_template(self): """Get template.""" tpl = self._template_cache @@ -357,14 +378,10 @@ class IndexView(HomeAssistantView): return tpl - async def get(self, request, extra=None): - """Serve the index view.""" + async def get(self, request: web.Request): + """Serve the index page for panel pages.""" hass = request.app['hass'] - if (request.path != '/' and - request.url.parts[1] not in hass.data[DATA_PANELS]): - raise web.HTTPNotFound - if not hass.components.onboarding.async_is_onboarded(): return web.Response(status=302, headers={ 'location': '/onboarding.html' @@ -383,6 +400,14 @@ class IndexView(HomeAssistantView): content_type='text/html' ) + def __len__(self) -> int: + """Return length of resource.""" + return 1 + + def __iter__(self): + """Iterate over routes.""" + return iter([self._route]) + class ManifestJSONView(HomeAssistantView): """View to return a manifest.json.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 09628b5d3fc..c362499db15 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -8,8 +8,7 @@ import pytest from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, - CONF_EXTRA_HTML_URL_ES5, generate_negative_index_regex, - EVENT_PANELS_UPDATED) + CONF_EXTRA_HTML_URL_ES5, EVENT_PANELS_UPDATED) from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro, async_capture_events @@ -348,43 +347,3 @@ async def test_auth_authorize(mock_http_client): resp = await mock_http_client.get(authorizejs.groups(0)[0]) assert resp.status == 200 assert 'public' in resp.headers.get('cache-control') - - -def test_index_regex(): - """Test the index regex.""" - pattern = re.compile('/' + generate_negative_index_regex()) - - for should_match in ( - '/', - '/lovelace', - '/lovelace/default_view', - '/map', - '/config', - ): - assert pattern.match(should_match), should_match - - for should_not_match in ( - '/service_worker.js', - '/manifest.json', - '/onboarding.html', - '/manifest.json', - 'static', - 'static/', - 'static/index.html', - 'frontend_latest', - 'frontend_latest/', - 'frontend_latest/index.html', - 'frontend_es5', - 'frontend_es5/', - 'frontend_es5/index.html', - 'local', - 'local/', - 'local/index.html', - 'auth', - 'auth/', - 'auth/index.html', - '/api', - '/api/', - '/api/logbook', - ): - assert not pattern.match(should_not_match), should_not_match