#5091 "Local assets". base.html, the dashboard and the share view loaded htmx and Chart.js from unpkg/jsdelivr, Gridstack from jsdelivr and their fonts from Google. On an instance with no outbound network (rule 164) every page lost htmx, and so every button and poll; and each page load told those hosts about the visitor (rule 155). - steward/static/vendor: htmx 2.0.4, Chart.js 4.4.4, Gridstack 12.6.0 (the versions the pages already used), with their licences (0BSD, MIT, MIT). - steward/static/fonts: Inter, Fraunces and JetBrains Mono, the design system's faces (SIL OFL, licences beside them), every unicode-range subset, with fonts.css. The serif face is now Fraunces in place of EB Garamond (base) and Libertinus Serif (share view); applying Inter and the type scale is the tokens step. - Templates use url_for('static'); /static already passes every gate. - Guard (tests/test_no_remote_assets.py): no template or stylesheet loads a script, stylesheet, font or image from another origin (comments stripped, example URLs in help text allowed), with a self-test that it can fail; every url_for('static') file and every font the sheet names exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.0 KiB
Python
72 lines
3.0 KiB
Python
"""Steward draws itself only with what it serves (#5091; rules 155 and 164).
|
|
|
|
No template or stylesheet loads a script, stylesheet, font or image from
|
|
another origin (a page would quietly lose htmx, its charts or its fonts on an
|
|
instance with no outbound network, and tell a third party about every
|
|
visitor), and every file a template asks url_for('static') for exists.
|
|
Example URLs in placeholders and help text are fine: only loads count.
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
import steward
|
|
|
|
_REPO = pathlib.Path(steward.__file__).parent.parent
|
|
_STATIC = _REPO / "steward" / "static"
|
|
|
|
_COMMENTS = re.compile(r"\{#.*?#\}|<!--.*?-->|/\*.*?\*/", re.S)
|
|
_REMOTE = r"""["']?\s*(?:https?:)?//"""
|
|
_LOADS = [
|
|
re.compile(r"<script\b[^>]*\bsrc\s*=\s*" + _REMOTE, re.I),
|
|
re.compile(r"<link\b[^>]*\bhref\s*=\s*" + _REMOTE, re.I),
|
|
re.compile(r"<img\b[^>]*\bsrc\s*=\s*" + _REMOTE, re.I),
|
|
re.compile(r"@import\s+" + _REMOTE, re.I), # @import url(...) is the url( case below
|
|
re.compile(r"\burl\(\s*" + _REMOTE, re.I),
|
|
]
|
|
_STATIC_REF = re.compile(r"url_for\(\s*'static'\s*,\s*filename\s*=\s*'([^']+)'\s*\)")
|
|
|
|
|
|
def remote_loads(text: str) -> list[str]:
|
|
text = _COMMENTS.sub("", text)
|
|
return [m.group(0) for rx in _LOADS for m in rx.finditer(text)]
|
|
|
|
|
|
def _files():
|
|
roots = [_REPO / "steward" / "templates", _STATIC, *(_REPO / "plugins").glob("*/templates")]
|
|
for root in roots:
|
|
for path in root.rglob("*"):
|
|
if path.suffix in (".html", ".css", ".js") and "vendor" not in path.parts:
|
|
yield path
|
|
|
|
|
|
def test_the_remote_asset_guard_can_fail():
|
|
page = """
|
|
{# <script src="https://cdn.example/explained.js"></script> #}
|
|
<!-- <link href="https://fonts.example/x.css" rel="stylesheet"> -->
|
|
<input placeholder="https://discord.com/api/webhooks/...">
|
|
<a href="https://github.com/org/repo">a link is not a load</a>
|
|
<script src="https://unpkg.com/htmx.org/dist/htmx.min.js"></script>
|
|
<link rel="preconnect" href="//fonts.gstatic.com">
|
|
<style>@import url(https://fonts.example/a.css); .x { background: url('http://x/y.png') }</style>
|
|
"""
|
|
assert len(remote_loads(page)) == 4
|
|
|
|
|
|
def test_nothing_is_loaded_from_another_origin():
|
|
found = {str(p.relative_to(_REPO)): hits for p in _files() if (hits := remote_loads(p.read_text()))}
|
|
assert not found, f"loads from another origin (vendor it under steward/static): {found}"
|
|
|
|
|
|
def test_every_static_file_a_template_names_exists():
|
|
refs = {(p, ref) for p in _files() for ref in _STATIC_REF.findall(p.read_text())}
|
|
assert any(ref == "vendor/htmx-2.0.4.min.js" for _, ref in refs)
|
|
missing = sorted(f"{p.relative_to(_REPO)}: {ref}" for p, ref in refs if not (_STATIC / ref).is_file())
|
|
assert not missing, missing
|
|
|
|
|
|
def test_every_font_file_the_font_sheet_names_exists():
|
|
sheet = (_STATIC / "fonts" / "fonts.css").read_text()
|
|
names = set(re.findall(r"url\(([^)]+\.woff2)\)", sheet))
|
|
assert len(names) >= 10
|
|
assert not [n for n in names if not (_STATIC / "fonts" / n).is_file()]
|