fix: expose iOS home screen icons for web app
This commit is contained in:
@@ -8,7 +8,20 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<meta name="theme-color" content="#03070b" />
|
<meta name="theme-color" content="#03070b" />
|
||||||
<link id="app-favicon" rel="icon" href="data:," sizes="any" />
|
<link id="app-favicon" rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png" />
|
||||||
|
<link
|
||||||
|
id="app-apple-touch-icon"
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon-precomposed"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/apple-touch-icon-precomposed.png"
|
||||||
|
/>
|
||||||
<title>/minishop</title>
|
<title>/minishop</title>
|
||||||
<link rel="stylesheet" href="/subscription_webapp.css" />
|
<link rel="stylesheet" href="/subscription_webapp.css" />
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -370,6 +370,78 @@ async def webapp_favicon_route(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
digest = str(request.match_info.get("digest") or "").strip().lower()
|
digest = str(request.match_info.get("digest") or "").strip().lower()
|
||||||
filename = str(request.match_info.get("filename") or "").strip()
|
filename = str(request.match_info.get("filename") or "").strip()
|
||||||
|
return _webapp_favicon_file_response(digest, filename)
|
||||||
|
|
||||||
|
|
||||||
|
async def webapp_current_favicon_route(request: web.Request) -> web.Response:
|
||||||
|
settings: Settings = request.app["settings"]
|
||||||
|
if not settings.WEBAPP_ENABLED:
|
||||||
|
raise web.HTTPNotFound(text="webapp_disabled")
|
||||||
|
|
||||||
|
requested_filename = str(request.path.rsplit("/", 1)[-1] or "").strip()
|
||||||
|
target_filename = _webapp_root_favicon_target_filename(requested_filename)
|
||||||
|
if not target_filename:
|
||||||
|
raise web.HTTPNotFound(text="webapp_favicon_not_found")
|
||||||
|
|
||||||
|
favicon_url = _resolve_webapp_favicon_url(settings, _resolve_webapp_logo_url(settings))
|
||||||
|
digest = _webapp_generated_favicon_digest(favicon_url)
|
||||||
|
if digest:
|
||||||
|
return _webapp_favicon_file_response(digest, target_filename)
|
||||||
|
|
||||||
|
redirect_url = _webapp_redirectable_favicon_url(favicon_url, target_filename)
|
||||||
|
if redirect_url:
|
||||||
|
raise web.HTTPFound(location=redirect_url)
|
||||||
|
|
||||||
|
raise web.HTTPNotFound(text="webapp_favicon_not_found")
|
||||||
|
|
||||||
|
|
||||||
|
def _webapp_root_favicon_target_filename(filename: str) -> str:
|
||||||
|
if filename == "apple-touch-icon-precomposed.png":
|
||||||
|
return "apple-touch-icon.png"
|
||||||
|
if filename in {
|
||||||
|
"apple-touch-icon.png",
|
||||||
|
"favicon.ico",
|
||||||
|
"icon-192.png",
|
||||||
|
"icon-512.png",
|
||||||
|
}:
|
||||||
|
return filename
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _webapp_generated_favicon_digest(favicon_url: str) -> str:
|
||||||
|
parsed = urlsplit(str(favicon_url or ""))
|
||||||
|
path = parsed.path if parsed.scheme or parsed.netloc else str(favicon_url or "")
|
||||||
|
match = re.fullmatch(
|
||||||
|
rf"{re.escape(WEBAPP_FAVICON_PATH)}/([0-9a-f]{{16}})/"
|
||||||
|
r"(?:icon-(?:16|32|48|180|192|512)\.png|apple-touch-icon\.png|favicon\.(?:ico|svg))",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
return match.group(1) if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _webapp_redirectable_favicon_url(favicon_url: str, target_filename: str) -> str:
|
||||||
|
href = str(favicon_url or "").strip()
|
||||||
|
if not href:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parsed = urlsplit(href)
|
||||||
|
path = parsed.path if parsed.scheme or parsed.netloc else href
|
||||||
|
suffix = Path(path).suffix.lower()
|
||||||
|
if target_filename in {"apple-touch-icon.png", "icon-192.png", "icon-512.png"}:
|
||||||
|
if suffix != ".png":
|
||||||
|
return ""
|
||||||
|
elif target_filename == "favicon.ico":
|
||||||
|
if suffix != ".ico":
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if parsed.scheme in {"http", "https"} or href.startswith("/"):
|
||||||
|
return href
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _webapp_favicon_file_response(digest: str, filename: str) -> web.Response:
|
||||||
if not re.fullmatch(r"[0-9a-f]{16}", digest):
|
if not re.fullmatch(r"[0-9a-f]{16}", digest):
|
||||||
raise web.HTTPNotFound(text="webapp_favicon_not_found")
|
raise web.HTTPNotFound(text="webapp_favicon_not_found")
|
||||||
if not re.fullmatch(
|
if not re.fullmatch(
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
|||||||
app.router.add_get("/auth/telegram/start", telegram_oauth_start_route)
|
app.router.add_get("/auth/telegram/start", telegram_oauth_start_route)
|
||||||
app.router.add_get("/auth/telegram/callback", telegram_oauth_callback_route)
|
app.router.add_get("/auth/telegram/callback", telegram_oauth_callback_route)
|
||||||
app.router.add_get("/health", health_route)
|
app.router.add_get("/health", health_route)
|
||||||
|
app.router.add_get("/favicon.ico", webapp_current_favicon_route)
|
||||||
|
app.router.add_get("/apple-touch-icon.png", webapp_current_favicon_route)
|
||||||
|
app.router.add_get("/apple-touch-icon-precomposed.png", webapp_current_favicon_route)
|
||||||
|
app.router.add_get("/icon-192.png", webapp_current_favicon_route)
|
||||||
|
app.router.add_get("/icon-512.png", webapp_current_favicon_route)
|
||||||
app.router.add_get(WEBAPP_LOGO_PROXY_PATH, webapp_logo_route)
|
app.router.add_get(WEBAPP_LOGO_PROXY_PATH, webapp_logo_route)
|
||||||
app.router.add_get(
|
app.router.add_get(
|
||||||
rf"{WEBAPP_UPLOADED_LOGO_PATH}/{{filename:[A-Za-z0-9_.-]+}}",
|
rf"{WEBAPP_UPLOADED_LOGO_PATH}/{{filename:[A-Za-z0-9_.-]+}}",
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/(favicon\.ico|apple-touch-icon(?:-precomposed)?\.png|icon-(?:192|512)\.png)$ {
|
||||||
|
proxy_pass http://backend:8081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location ~ ^/(webapp-logo|webapp-uploaded-logo|webapp-favicon|webapp-emoji|webapp-theme-css|webapp-theme-assets)/ {
|
location ~ ^/(webapp-logo|webapp-uploaded-logo|webapp-favicon|webapp-emoji|webapp-theme-css|webapp-theme-assets)/ {
|
||||||
proxy_pass http://backend:8081;
|
proxy_pass http://backend:8081;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -210,6 +210,48 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertTrue((Path(tmpdir) / digest / "apple-touch-icon.png").exists())
|
self.assertTrue((Path(tmpdir) / digest / "apple-touch-icon.png").exists())
|
||||||
self.assertTrue((Path(tmpdir) / digest / "favicon.ico").exists())
|
self.assertTrue((Path(tmpdir) / digest / "favicon.ico").exists())
|
||||||
|
|
||||||
|
async def test_current_favicon_alias_serves_generated_apple_touch_icon(self):
|
||||||
|
digest = "abcdef1234567890"
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
icon_dir = Path(tmpdir) / digest
|
||||||
|
icon_dir.mkdir()
|
||||||
|
(icon_dir / "apple-touch-icon.png").write_bytes(b"touch-icon")
|
||||||
|
settings = SimpleNamespace(
|
||||||
|
WEBAPP_ENABLED=True,
|
||||||
|
WEBAPP_LOGO_URL="",
|
||||||
|
WEBAPP_LOGO_USE_EMOJI=False,
|
||||||
|
WEBAPP_FAVICON_USE_CUSTOM=True,
|
||||||
|
WEBAPP_FAVICON_URL=f"/webapp-favicon/{digest}/icon-180.png",
|
||||||
|
WEBAPP_LOGO_FAVICON_URL="",
|
||||||
|
)
|
||||||
|
request = SimpleNamespace(
|
||||||
|
app={"settings": settings},
|
||||||
|
path="/apple-touch-icon-precomposed.png",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(webapp_assets, "WEBAPP_FAVICON_DIR", Path(tmpdir)):
|
||||||
|
response = await webapp_assets.webapp_current_favicon_route(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status, 200)
|
||||||
|
self.assertEqual(response.content_type, "image/png")
|
||||||
|
self.assertEqual(response.body, b"touch-icon")
|
||||||
|
|
||||||
|
def test_static_webapp_template_exposes_ios_icon_aliases(self):
|
||||||
|
template = Path("backend/bot/app/web/templates/subscription_webapp.html").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('rel="apple-touch-icon"', template)
|
||||||
|
self.assertIn('href="/apple-touch-icon.png"', template)
|
||||||
|
self.assertIn('href="/favicon.ico"', template)
|
||||||
|
|
||||||
|
def test_frontend_nginx_proxies_root_icon_aliases(self):
|
||||||
|
nginx_conf = Path("deploy/docker/frontend/nginx.conf").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("apple-touch-icon", nginx_conf)
|
||||||
|
self.assertIn("favicon\\.ico", nginx_conf)
|
||||||
|
self.assertIn("proxy_pass http://backend:8081;", nginx_conf)
|
||||||
|
|
||||||
def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self):
|
def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self):
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
root = Path(tmpdir)
|
root = Path(tmpdir)
|
||||||
|
|||||||
@@ -221,6 +221,22 @@ class WebAppRouteContractTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(match_info.handler.__name__, "webapp_favicon_route")
|
self.assertEqual(match_info.handler.__name__, "webapp_favicon_route")
|
||||||
|
|
||||||
|
def test_webapp_root_icon_alias_routes_are_registered(self):
|
||||||
|
app = web.Application()
|
||||||
|
subscription_webapp.setup_subscription_webapp_routes(app)
|
||||||
|
|
||||||
|
for path in (
|
||||||
|
"/favicon.ico",
|
||||||
|
"/apple-touch-icon.png",
|
||||||
|
"/apple-touch-icon-precomposed.png",
|
||||||
|
"/icon-192.png",
|
||||||
|
"/icon-512.png",
|
||||||
|
):
|
||||||
|
request = make_mocked_request("GET", path, app=app)
|
||||||
|
match_info = asyncio.run(app.router.resolve(request))
|
||||||
|
|
||||||
|
self.assertEqual(match_info.handler.__name__, "webapp_current_favicon_route")
|
||||||
|
|
||||||
def test_app_deeplink_gateway_keeps_target_in_fragment(self):
|
def test_app_deeplink_gateway_keeps_target_in_fragment(self):
|
||||||
request = _Request(
|
request = _Request(
|
||||||
app={
|
app={
|
||||||
|
|||||||
Reference in New Issue
Block a user