diff --git a/apps/mobile/src/components/LeafletView.tsx b/apps/mobile/src/components/LeafletView.tsx index 39de4f6..3525696 100644 --- a/apps/mobile/src/components/LeafletView.tsx +++ b/apps/mobile/src/components/LeafletView.tsx @@ -3,8 +3,15 @@ import { View } from "react-native"; import { WebView as RNWebView, type WebViewMessageEvent, type WebViewProps } from "react-native-webview"; import { useOnline } from "../net"; import { Empty, IconButton, makeStyles, useTheme } from "../ui"; +import { API_URL } from "../config"; import { mapPage } from "./mapHtml"; +/** + * The map page's origin: the instance's own (issue #281). It is the Referer of the tile requests, and + * OpenStreetMap's usage policy asks for a real one; a made-up host got the tiles blocked. + */ +const PAGE_ORIGIN = API_URL ? `${new URL(API_URL).origin}/` : "about:blank"; + // react-native-webview's class typings collapse to `never` under this TS/React combination; use the props type directly. const WebView = RNWebView as unknown as ComponentType void }> }>; @@ -19,7 +26,7 @@ export function LeafletView({ script, onMessage, fitLabel = "Show all", offlineA const { c, name } = useTheme(); const online = useOnline(); const ref = useRef<{ injectJavaScript: (js: string) => void } | null>(null); - const source = useMemo(() => ({ html: mapPage({ dark: name === "set", bg: c.bg, script }), baseUrl: "https://openclapper.local/" }), [name, c.bg, script]); + const source = useMemo(() => ({ html: mapPage({ dark: name === "set", bg: c.bg, script }), baseUrl: PAGE_ORIGIN }), [name, c.bg, script]); const run = (js: string) => ref.current?.injectJavaScript(`${js};true;`); if (handle) handle.current = { run }; if (!online) { diff --git a/apps/mobile/src/components/mapHtml.ts b/apps/mobile/src/components/mapHtml.ts index 1b47686..16463d5 100644 --- a/apps/mobile/src/components/mapHtml.ts +++ b/apps/mobile/src/components/mapHtml.ts @@ -27,7 +27,7 @@ export function mapPage({ dark, bg, script }: { dark: boolean; bg: string; scrip
`; diff --git a/apps/studio/src/Locations.tsx b/apps/studio/src/Locations.tsx index 85019d0..2b13644 100644 --- a/apps/studio/src/Locations.tsx +++ b/apps/studio/src/Locations.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "preact/hooks"; import L from "leaflet"; +import { osmTiles } from "./mapTiles"; import "leaflet/dist/leaflet.css"; import { ApiError, deleteLocation, deleteSpot, existingIdOf, putLocation, putSpot, type Location, type Shot, type Spot } from "./api"; import { binned } from "./apps/trashOps"; @@ -187,7 +188,7 @@ function PinDialog({ title, place, shots, onClose, onSave }: { title: string; pl useEffect(() => { if (!el.current) return; const map = L.map(el.current, { zoomControl: true }).setView(centre, at || own.length ? 17 : 12); - L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors" }).addTo(map); + osmTiles().addTo(map); for (const { s, p } of own) { L.circleMarker([p.lat, p.lon], { radius: 6, color: "#0B0B0C", weight: 2, fillColor: s.position_from_location ? "#FFB000" : "#FFFFFF", fillOpacity: 1 }) .bindTooltip(`${shotTitle(s)}${s.position_from_location ? " ยท synced to the place" : ""}`).addTo(map); diff --git a/apps/studio/src/MapView.tsx b/apps/studio/src/MapView.tsx index 248ffcf..c11f379 100644 --- a/apps/studio/src/MapView.tsx +++ b/apps/studio/src/MapView.tsx @@ -1,6 +1,7 @@ import { render } from "preact"; import { useEffect, useRef } from "preact/hooks"; import L from "leaflet"; +import { osmTiles } from "./mapTiles"; import "leaflet/dist/leaflet.css"; import { label, STATE_ICONS } from "@openclapper/vocab"; import type { Shot } from "./api"; @@ -66,7 +67,7 @@ function LeafletMap({ shots, mask, selectedId, onSelect, onOpen }: Props) { useEffect(() => { if (!el.current) return; const m = L.map(el.current, { zoomControl: false }); - L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(m); + osmTiles().addTo(m); L.control.zoom({ position: "topleft" }).addTo(m); map.current = m; m.setView([51, 10], 5); diff --git a/apps/studio/src/PositionEditor.tsx b/apps/studio/src/PositionEditor.tsx index 2f86156..a29ffed 100644 --- a/apps/studio/src/PositionEditor.tsx +++ b/apps/studio/src/PositionEditor.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "preact/hooks"; import L from "leaflet"; +import { osmTiles } from "./mapTiles"; import "leaflet/dist/leaflet.css"; import { patchPhotoPosition, type Photo, type Shot } from "./api"; import { positionOf } from "./format"; @@ -31,7 +32,7 @@ export function PositionStage({ shot, photo, onSaved, onCancel, onError }: Props useEffect(() => { if (!el.current) return; const map = L.map(el.current, { zoomControl: false }).setView(start, had || positionOf(shot) ? 18 : 12); - L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors" }).addTo(map); + osmTiles().addTo(map); L.control.zoom({ position: "topleft" }).addTo(map); // Reported accuracy: dashed ring with a faint accent fill, readable on light and (filtered) dark tiles. if (had && photo.gps_accuracy_m) L.circle(start, { radius: photo.gps_accuracy_m, color: "#0B0B0C", weight: 2, dashArray: "6 4", fillColor: "#0040D8", fillOpacity: 0.12 }).addTo(map); diff --git a/apps/studio/src/mapTiles.ts b/apps/studio/src/mapTiles.ts new file mode 100644 index 0000000..b259832 --- /dev/null +++ b/apps/studio/src/mapTiles.ts @@ -0,0 +1,12 @@ +import L from "leaflet"; + +/** + * OpenStreetMap's tiles with what its usage policy asks of a web page (issue #281): attribution and a + * valid Referer. The page's own policy is `same-origin` (service-kit), which sends no Referer to the + * tile server and gets the tiles blocked; the tiles send the page's origin, nothing more. + */ +export const osmTiles = () => L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + attribution: '© OpenStreetMap contributors', + referrerPolicy: "strict-origin-when-cross-origin", +}); diff --git a/docs/infrastructure.md b/docs/infrastructure.md index a4380ce..a85a86d 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -12,7 +12,7 @@ Everything runs on one Cloudflare account per instance; nothing else is needed. | Cloudflare Access | The admin plane's sign-in. | | Android signing key | Per deployment, made once with `scripts/keystore.sh` (into a folder outside the checkout); in the deployment's GitHub secrets and a backup, never in git or a checkout. The staging app (`org.openclapper.staging`) uses the same key. Builds need no other service; the only build-time setting is `EXPO_PUBLIC_API_URL` ([self-hosting](self-hosting.md#the-phone)) | | Open-Meteo | Weather forecast, called from the Studio browser directly (free, no key); the only origin besides the tiles in the Studio's CSP `connect-src` | -| OpenStreetMap tiles | Maps in the Studio (Leaflet) and the phone (Leaflet in a WebView, loaded from unpkg) | +| OpenStreetMap tiles | Maps in the Studio (Leaflet, `mapTiles.ts`) and the phone (Leaflet in a WebView, loaded from unpkg). Straight from `tile.openstreetmap.org` under its [usage policy](https://operations.osmfoundation.org/policies/tiles/): attribution and a valid Referer. The Studio's page policy is `same-origin`, so the tile layers send the origin themselves; the phone's map page takes the instance's origin as its base URL (issue #281) | ## Auth