diff --git a/drb-c2-core/app/internal/cameras/regional.py b/drb-c2-core/app/internal/cameras/regional.py index a128182..49d12a6 100644 --- a/drb-c2-core/app/internal/cameras/regional.py +++ b/drb-c2-core/app/internal/cameras/regional.py @@ -62,9 +62,13 @@ class Ny511Provider(CameraProvider): async def fetch_catalog(self) -> List[Camera]: rows = await ny511._fetch("getcameras", ny511.normalize_camera) + # Hosts for the 511x imagery are unverified, so pin the scheme at least: + # only https URLs reach the browser. + def https(u): + return u if isinstance(u, str) and u.startswith("https://") else None out = [ make_camera("ny511", r["id"], r["lat"], r["lon"], name=r["name"], roadway=r["roadway"], - direction=r["direction"], image_url=r["image_url"], video_url=r["video_url"]) + direction=r["direction"], image_url=https(r["image_url"]), video_url=https(r["video_url"])) for r in rows ] return require_parsed(len(rows), out, "511NY") diff --git a/drb-c2-core/app/internal/cameras/registry.py b/drb-c2-core/app/internal/cameras/registry.py index f58e873..e3f761c 100644 --- a/drb-c2-core/app/internal/cameras/registry.py +++ b/drb-c2-core/app/internal/cameras/registry.py @@ -181,18 +181,20 @@ async def cameras_in_bbox(bbox: BBox) -> dict: """Cameras from every SERVING provider whose coverage meets ``bbox``. Providers that are off are never fetched.""" conf = await get_settings() - live = [pid for pid, cat in CATALOGS.items() - if effective(PROVIDERS[pid], conf.get(pid))[0] and bbox_intersects(PROVIDERS[pid].meta.coverage, bbox)] + serving = [pid for pid in CATALOGS if effective(PROVIDERS[pid], conf.get(pid))[0]] + live = [pid for pid in serving if bbox_intersects(PROVIDERS[pid].meta.coverage, bbox)] await asyncio.gather(*(CATALOGS[pid].get() for pid in live)) cams: List[Camera] = [] providers = [] - for pid in live: + # Every SERVING provider is listed (credits included) even when the bbox + # misses it, so a client holding one of its cameras from another view can + # still render the required attribution. Only intersecting ones are fetched. + for pid in serving: cat = CATALOGS[pid] - hits = in_bbox(cat.items, bbox) + hits = in_bbox(cat.items, bbox) if pid in live else [] room = max(0, MAX_ITEMS - len(cams)) cams.extend(hits[:room]) providers.append({"id": pid, "name": PROVIDERS[pid].meta.name, **credit(pid), "fetched_at": cat.fetched_at, "error": cat.error, "total_in_bbox": len(hits), "truncated": len(hits) > room}) - return {"cameras": cams, "providers": providers, - "serving_count": sum(1 for pid in CATALOGS if effective(PROVIDERS[pid], conf.get(pid))[0])} + return {"cameras": cams, "providers": providers, "serving_count": len(serving)} diff --git a/drb-c2-core/tests/test_camera_providers.py b/drb-c2-core/tests/test_camera_providers.py index 9b3af0c..5d5e5a8 100644 --- a/drb-c2-core/tests/test_camera_providers.py +++ b/drb-c2-core/tests/test_camera_providers.py @@ -148,8 +148,12 @@ def test_query_returns_attribution_and_skips_providers_outside_bbox(store): patch.object(registry.PROVIDERS["tfl"], "fetch_catalog", tfl_fetch): out = run(registry.cameras_in_bbox((-34.0, 151.0, -33.7, 151.4))) assert [c["id"] for c in out["cameras"]] == ["nsw:x"] - assert out["providers"][0]["attribution"] == "Transport for NSW, CC BY 4.0" + by_id = {p["id"]: p for p in out["providers"]} + assert by_id["nsw"]["attribution"] == "Transport for NSW, CC BY 4.0" assert tfl_fetch.await_count == 0 # London never fetched for a Sydney bbox + # ...but its credit is still listed, for cameras the client holds from another view + assert by_id["tfl"]["attribution"] == "Powered by TfL Open Data" and by_id["tfl"]["total_in_bbox"] == 0 + assert out["serving_count"] == 2 # ── admin routes ───────────────────────────────────────────────────────────── @@ -263,10 +267,11 @@ def test_ontario_needs_its_key(monkeypatch): def test_ny511_provider_wraps_legacy_client_and_carries_daa_metadata(): row = {"id": "NYSDOT-1", "lat": 41.03, "lon": -73.76, "name": "I-287", "roadway": "I-287", - "direction": "Northbound", "image_url": "https://x/1", "video_url": None} + "direction": "Northbound", "image_url": "https://x/1", "video_url": "javascript:alert(1)"} with patch.object(regional.ny511, "_fetch", AsyncMock(return_value=[row])): cams = run(regional.Ny511Provider().fetch_catalog()) assert cams[0]["id"] == "ny511:NYSDOT-1" and cams[0]["heading_deg"] == 0 + assert cams[0]["video_url"] is None # non-https never reaches the browser lic = regional.Ny511Provider.meta.licence assert lic.attribution == "powered by 511NY" and lic.attribution_url == "https://511ny.org" assert "with all faults" in lic.disclaimer and lic.gated