|
12 | 12 | cannot silently leave a page with a dangling warning. |
13 | 13 | 4. Apply the same check to the `x-phyphox-inconsistency` keys in the OpenAPI |
14 | 14 | description, so the spec and the to-do list cannot drift either. |
| 15 | +5. Fail the build if the generated site would make a visitor's browser fetch |
| 16 | + anything from a third party. |
15 | 17 | """ |
16 | 18 |
|
17 | 19 | import os |
@@ -209,3 +211,71 @@ def _render_list(): |
209 | 211 | if e.get("issue"): |
210 | 212 | out.append(f"[Tracking issue]({e['issue']})\n") |
211 | 213 | return "\n".join(out) |
| 214 | + |
| 215 | + |
| 216 | +# -------------------------------------------------------------------------- |
| 217 | +# No third-party requests |
| 218 | +# -------------------------------------------------------------------------- |
| 219 | +# |
| 220 | +# Visitors must be able to read this site without their browser contacting |
| 221 | +# anyone but the host serving it. That is easy to lose by accident: Material |
| 222 | +# pulls Roboto from fonts.googleapis.com unless `font: false` is set, mounts a |
| 223 | +# component that calls api.github.com when repo_url is present, and Swagger UI |
| 224 | +# ships a validator badge that posts the spec URL to validator.swagger.io. All |
| 225 | +# three are switched off - this check is what stops them coming back unnoticed |
| 226 | +# on the next dependency bump. |
| 227 | +# |
| 228 | +# Only *automatic* fetches count. Ordinary hyperlinks are fine; a visitor |
| 229 | +# choosing to follow one is not the site phoning home. |
| 230 | + |
| 231 | +_RESOURCE_TAG = re.compile( |
| 232 | + r"<(link|script|img|iframe|source|video|audio|embed|object)\b([^>]*)>", re.I) |
| 233 | +_URL_ATTR = re.compile(r"(?:src|href|data)\s*=\s*[\"']([^\"']+)[\"']", re.I) |
| 234 | +_REL_ATTR = re.compile(r"rel\s*=\s*[\"']([^\"']+)[\"']", re.I) |
| 235 | +_CSS_URL = re.compile(r"(?:url\(\s*[\"']?|@import\s+[\"'])(https?:)?//([^)\"'\s]+)") |
| 236 | + |
| 237 | +# rel values that describe a relationship without fetching anything. |
| 238 | +_NON_FETCHING_RELS = {"canonical", "alternate", "author", "license", "me", |
| 239 | + "nofollow", "noopener", "noreferrer"} |
| 240 | + |
| 241 | + |
| 242 | +def _is_absolute(url): |
| 243 | + return url.startswith(("http://", "https://", "//")) |
| 244 | + |
| 245 | + |
| 246 | +def on_post_build(config, **kwargs): |
| 247 | + site_dir = config["site_dir"] |
| 248 | + offenders = [] |
| 249 | + |
| 250 | + for root, _, files in os.walk(site_dir): |
| 251 | + for fn in files: |
| 252 | + path = os.path.join(root, fn) |
| 253 | + rel_path = os.path.relpath(path, site_dir) |
| 254 | + if fn.endswith((".html", ".htm")): |
| 255 | + with open(path, encoding="utf-8", errors="ignore") as f: |
| 256 | + text = f.read() |
| 257 | + for m in _RESOURCE_TAG.finditer(text): |
| 258 | + tag, attrs = m.group(1).lower(), m.group(2) |
| 259 | + url = _URL_ATTR.search(attrs) |
| 260 | + if not url or not _is_absolute(url.group(1)): |
| 261 | + continue |
| 262 | + rel = _REL_ATTR.search(attrs) |
| 263 | + rels = set((rel.group(1) if rel else "").lower().split()) |
| 264 | + if rels & _NON_FETCHING_RELS: |
| 265 | + continue |
| 266 | + offenders.append(f"{rel_path}: <{tag}> {url.group(1)}") |
| 267 | + elif fn.endswith(".css"): |
| 268 | + with open(path, encoding="utf-8", errors="ignore") as f: |
| 269 | + text = f.read() |
| 270 | + for m in _CSS_URL.finditer(text): |
| 271 | + offenders.append(f"{rel_path}: css url() //{m.group(2)}") |
| 272 | + |
| 273 | + if offenders: |
| 274 | + shown = "\n".join(f" {o}" for o in sorted(set(offenders))[:20]) |
| 275 | + more = len(set(offenders)) - 20 |
| 276 | + raise ValueError( |
| 277 | + "The built site would make visitors' browsers fetch from a third " |
| 278 | + "party:\n" + shown |
| 279 | + + (f"\n ... and {more} more" if more > 0 else "") |
| 280 | + + "\n\nEverything the site needs must be served from the site " |
| 281 | + "itself. See the 'No third-party requests' note in tools/hooks.py.") |
0 commit comments