Skip to content
Merged
45 changes: 37 additions & 8 deletions apps/geolibre-desktop/src/components/layout/AddDataDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ interface AddDataDialogProps {
* clicked PostGIS table.
*/
initialPostgres?: OpenAddDataPostgres;
/** Service URL supplied by a browser-extension deep link. */
initialUrl?: string;
/**
* The layer that deep link asked for — a WMS `LAYERS` value, a WFS feature
* type, a vector tile source layer. Prefilled so the form the extension opens
* is complete rather than an endpoint the user must still name a layer on.
*/
initialLayer?: string;
/** Style document accompanying a deep-linked vector tileset. */
initialStyleUrl?: string;
}

/**
Expand All @@ -55,20 +65,29 @@ function renderSource(
kind: AddDataKind,
initialDeckVizKind: string | undefined,
initialPostgres: OpenAddDataPostgres | undefined,
initialUrl: string | undefined,
initialLayer: string | undefined,
initialStyleUrl: string | undefined,
) {
switch (kind) {
case "xyz":
return <XyzSource />;
return <XyzSource initialUrl={initialUrl} />;
case "wms":
return <WmsSource />;
return <WmsSource initialUrl={initialUrl} initialLayers={initialLayer} />;
case "wfs":
return <WfsSource />;
return <WfsSource initialUrl={initialUrl} initialTypeName={initialLayer} />;
case "wmts":
return <WmtsSource />;
return <WmtsSource initialUrl={initialUrl} />;
case "ogc-features":
return <OgcFeaturesSource />;
return <OgcFeaturesSource initialUrl={initialUrl} />;
case "ogc-vector-tiles":
return <OgcVectorTilesSource />;
return (
<OgcVectorTilesSource
initialUrl={initialUrl}
initialStyleUrl={initialStyleUrl}
initialSourceLayers={initialLayer}
/>
);
case "gpx":
return <GpxSource />;
case "georss":
Expand All @@ -84,7 +103,7 @@ function renderSource(
case "mbtiles":
return <MbtilesSource />;
case "arcgis":
return <ArcGISSource />;
return <ArcGISSource initialUrl={initialUrl} />;
case "postgres":
return <PostgresSource initialPostgres={initialPostgres} />;
case "video":
Expand All @@ -107,6 +126,9 @@ export function AddDataDialog({
onOpenChange,
initialDeckVizKind,
initialPostgres,
initialUrl,
initialLayer,
initialStyleUrl,
}: AddDataDialogProps) {
const { t } = useTranslation();
const open = kind !== null;
Expand Down Expand Up @@ -158,7 +180,14 @@ export function AddDataDialog({

{kind ? (
<AddDataShellProvider value={contextValue}>
{renderSource(kind, initialDeckVizKind, initialPostgres)}
{renderSource(
kind,
initialDeckVizKind,
initialPostgres,
initialUrl,
initialLayer,
initialStyleUrl,
)}
</AddDataShellProvider>
) : null}
</DialogContent>
Expand Down
16 changes: 15 additions & 1 deletion apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { IS_MAS_BUILD } from "../../lib/build-flags";
import { masHidesDataSource } from "../../lib/mas-build";
import { IS_STORE_BUILD } from "../../lib/updates";
import { AddDataDialog, type AddDataKind } from "./AddDataDialog";
import { serviceUrlParameter } from "../../lib/data-url";
import {
OPEN_ADD_DATA_EVENT,
type OpenAddDataDetail,
Expand Down Expand Up @@ -1029,7 +1030,12 @@ export function TopToolbar({
{} as Record<ToolbarMapControl, boolean>,
),
);
const [addDataKind, setAddDataKind] = useState<AddDataKind | null>(null);
const [initialService, setInitialService] = useState(() =>
viewer || typeof window === "undefined" ? null : serviceUrlParameter(window.location.search),
);
const [addDataKind, setAddDataKind] = useState<AddDataKind | null>(
() => (initialService?.kind as AddDataKind | undefined) ?? null,
);
Comment thread
giswqs marked this conversation as resolved.
Outdated
const [addDataTargetGroupId, setAddDataTargetGroupId] = useState<string | null>(null);
const addDataInitialLayerIdsRef = useRef<Set<string>>(new Set());
// Every path that opens the dialog outside the OPEN_ADD_DATA_EVENT listener
Expand Down Expand Up @@ -2085,6 +2091,13 @@ export function TopToolbar({
mapControllerRef={mapControllerRef}
initialDeckVizKind={addDataDeckVizKind}
initialPostgres={addDataPostgres}
initialUrl={addDataKind === initialService?.kind ? initialService.url : undefined}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
giswqs marked this conversation as resolved.
initialLayer={
addDataKind === initialService?.kind ? (initialService.layer ?? undefined) : undefined
}
initialStyleUrl={
addDataKind === initialService?.kind ? (initialService.styleUrl ?? undefined) : undefined
}
onOpenChange={(open: boolean) => {
if (!open) {
if (addDataTargetGroupId) {
Expand All @@ -2097,6 +2110,7 @@ export function TopToolbar({
}
}
setAddDataKind(null);
setInitialService(null);
setAddDataTargetGroupId(null);
setAddDataDeckVizKind(undefined);
setAddDataPostgres(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ const URL_PLACEHOLDER_KEYS = {
"image-service": "addData.arcgis.imageServiceUrlPlaceholder",
} as const satisfies Record<ArcGISLayerType, string>;

export function ArcGISSource() {
export function ArcGISSource({ initialUrl = "" }: { initialUrl?: string }) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.arcgis.defaultName"));
const [arcgisLayerType, setArcgisLayerType] = useState<ArcGISLayerType>("feature");
const [arcgisSourceType, setArcgisSourceType] = useState<ArcGISSourceType>("url");
const [arcgisUrl, setArcgisUrl] = useState("");
const [arcgisUrl, setArcgisUrl] = useState(initialUrl);
const [arcgisItemId, setArcgisItemId] = useState("");
const [arcgisPortalUrl, setArcgisPortalUrl] = useState("");
const [arcgisAccessToken, setArcgisAccessToken] = useState("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ interface OgcFeaturesSample {
* feature count is reached, because a single `/items` request returns only one
* server-sized page.
*/
export function OgcFeaturesSource() {
export function OgcFeaturesSource({ initialUrl = "" }: { initialUrl?: string }) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.ogcFeatures.defaultName"));
const [endpoint, setEndpoint] = useState(ogcFeaturesFormCache?.endpoint ?? "");
const [endpoint, setEndpoint] = useState(initialUrl || ogcFeaturesFormCache?.endpoint || "");
const [collectionId, setCollectionId] = useState(ogcFeaturesFormCache?.collectionId ?? "");
Comment thread
giswqs marked this conversation as resolved.
Outdated
const [maxFeatures, setMaxFeatures] = useState(
ogcFeaturesFormCache?.maxFeatures ?? String(DEFAULT_OGC_FEATURES_MAX_FEATURES),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ interface OgcSample {
* API TileJSON often omits them). Rendering uses GeoLibre's default per-source
* layer styling; the style's own paint is not applied.
*/
export function OgcVectorTilesSource() {
export function OgcVectorTilesSource({
initialUrl = "",
initialStyleUrl = "",
initialSourceLayers = "",
}: {
initialUrl?: string;
initialStyleUrl?: string;
initialSourceLayers?: string;
}) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.ogcVectorTiles.defaultName"));
const [tilesUrl, setTilesUrl] = useState("");
const [styleUrl, setStyleUrl] = useState("");
const [sourceLayersText, setSourceLayersText] = useState("");
const [tilesUrl, setTilesUrl] = useState(initialUrl);
const [styleUrl, setStyleUrl] = useState(initialStyleUrl);
const [sourceLayersText, setSourceLayersText] = useState(initialSourceLayers);

// Cancel the in-flight metadata/style/collections fetches if the dialog closes
// mid-request, so a slow response cannot add the layer after the user leaves.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ interface WfsFormCache {
}
let wfsFormCache: WfsFormCache | null = null;

export function WfsSource() {
export function WfsSource({
initialUrl = "",
initialTypeName = "",
}: {
initialUrl?: string;
initialTypeName?: string;
}) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.wfs.defaultName"));
const [wfsEndpoint, setWfsEndpoint] = useState(wfsFormCache?.endpoint ?? "");
const [wfsTypeName, setWfsTypeName] = useState(wfsFormCache?.typeName ?? "");
const [wfsEndpoint, setWfsEndpoint] = useState(initialUrl || wfsFormCache?.endpoint || "");
const [wfsTypeName, setWfsTypeName] = useState(initialTypeName || (wfsFormCache?.typeName ?? ""));
Comment thread
giswqs marked this conversation as resolved.
Outdated
const [wfsVersion, setWfsVersion] = useState(wfsFormCache?.version ?? "2.0.0");
const [wfsOutputFormat, setWfsOutputFormat] = useState(
wfsFormCache?.outputFormat ?? "application/json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ interface WmsFormCache {
}
let wmsFormCache: WmsFormCache | null = null;

export function WmsSource() {
export function WmsSource({
initialUrl = "",
initialLayers = "",
}: {
initialUrl?: string;
initialLayers?: string;
}) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.wms.defaultName"));
const [wmsEndpoint, setWmsEndpoint] = useState(wmsFormCache?.endpoint ?? "");
const [wmsLayers, setWmsLayers] = useState(wmsFormCache?.layers ?? "");
const [wmsEndpoint, setWmsEndpoint] = useState(initialUrl || wmsFormCache?.endpoint || "");
const [wmsLayers, setWmsLayers] = useState(initialLayers || (wmsFormCache?.layers ?? ""));
Comment thread
giswqs marked this conversation as resolved.
Outdated
const [wmsStyles, setWmsStyles] = useState(wmsFormCache?.styles ?? "");
const [wmsFormat, setWmsFormat] = useState(wmsFormCache?.format ?? "image/png");
const [wmsTransparent, setWmsTransparent] = useState(wmsFormCache?.transparent ?? true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ServiceLibrarySection } from "../ServiceLibrarySection";
import { serviceFieldString, type ServiceFields } from "../service-library";
import { AddDataSourceForm, SampleDataSelect, useAddDataSource } from "../shared";

export function WmtsSource() {
export function WmtsSource({ initialUrl = "" }: { initialUrl?: string }) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.wmts.defaultName"));
const [wmtsUrl, setWmtsUrl] = useState("");
const [wmtsUrl, setWmtsUrl] = useState(initialUrl);
const [wmtsTileSize, setWmtsTileSize] = useState("256");

const getFields = (): ServiceFields => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { ServiceLibrarySection } from "../ServiceLibrarySection";
import { serviceFieldBoolean, serviceFieldString, type ServiceFields } from "../service-library";
import { AddDataSourceForm, SampleDataSelect, useAddDataSource } from "../shared";

export function XyzSource() {
export function XyzSource({ initialUrl = "" }: { initialUrl?: string }) {
const { t } = useTranslation();
const source = useAddDataSource(t("addData.xyz.defaultName"));
const [xyzUrl, setXyzUrl] = useState("");
const [xyzUrl, setXyzUrl] = useState(initialUrl);
const [xyzTileSize, setXyzTileSize] = useState("256");
const [xyzShortUrl, setXyzShortUrl] = useState(false);

Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/hooks/useStartupProject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAppStore, type MapProjection } from "@geolibre/core";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { dataUrlParameters } from "../lib/data-url";
import { dataUrlParameters, serviceUrlParameter } from "../lib/data-url";
import { isTauri } from "../lib/is-tauri";
import { projectUrlFromLocation } from "../lib/project-url";
import { planStartup, startupDefaultProjection, type StartupPlan } from "../lib/startup-project";
Expand Down Expand Up @@ -36,6 +36,7 @@ const RESTORE_GATE_TIMEOUT_MS = 10_000;
function hasExplicitLaunchPayload(): boolean {
if (projectUrlFromLocation() !== null) return true;
if (dataUrlParameters(window.location.search) !== null) return true;
if (serviceUrlParameter(window.location.search) !== null) return true;
return false;
}

Expand Down
32 changes: 32 additions & 0 deletions apps/geolibre-desktop/src/lib/data-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ export interface DataUrlParameter {
dataUrl: string;
styleUrl: string | null;
}
const SERVICE_KINDS = new Set([
"xyz",
"wms",
"wmts",
"wfs",
"ogc-features",
"ogc-vector-tiles",
"arcgis",
]);

export interface ServiceUrlParameter {
kind: string;
url: string;
/** The requested layer: WMS `LAYERS`, WFS `typeName`, a tile source layer. */
layer: string | null;
/** A style document naming the source layers of a vector tileset. */
styleUrl: string | null;
}

export function serviceUrlParameter(search: string): ServiceUrlParameter | null {
const params = new URLSearchParams(search);
const kind = params.get("add");
const rawUrl = params.get("serviceUrl");
const url = httpUrl(rawUrl)?.replace(/%7B/gi, "{").replace(/%7D/gi, "}") ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serviceUrlParameter restores %7B/%7D{/} across the whole decoded serviceUrl, for every service kind — not just xyz/wmts/ogc-vector-tiles, which are the only kinds that actually use {z}/{x}/{y} tile templates. For wms, wfs, ogc-features, and arcgis deep links, this blanket replace can silently corrupt a URL whose query string legitimately contains those percent-encoded sequences for an unrelated reason (e.g. an auth token or signed parameter that happens to encode {/}), turning them into literal braces the origin server never sent.

Consider scoping the placeholder restoration to only the kinds that need it:

Suggested change
const url = httpUrl(rawUrl)?.replace(/%7B/gi, "{").replace(/%7D/gi, "}") ?? null;
const needsTileBraces = kind === "xyz" || kind === "wmts" || kind === "ogc-vector-tiles";
const url = httpUrl(rawUrl);
const restoredUrl =
url && needsTileBraces ? url.replace(/%7B/gi, "{").replace(/%7D/gi, "}") : url;

(and use restoredUrl below instead of url, with the !kind || !SERVICE_KINDS.has(kind) check moved before this to know kind first).

Confidence: medium — narrow real-world trigger, but a real correctness gap introduced by this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #1969 (landed after this PR merged) — brace restoration is now limited to xyz/wmts/ogc-vector-tiles, with a test asserting a WMS token keeps its encoded braces.

if (!kind || !SERVICE_KINDS.has(kind) || !url) return null;
return {
kind,
url,
layer: params.get("serviceLayer")?.trim() || null,
styleUrl: httpUrl(params.get("serviceStyle")),
};
}
export interface RemoteGeoJsonLayer {
data: FeatureCollection;
name: string;
Expand Down
17 changes: 10 additions & 7 deletions extensions/geolibre-chrome/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Privacy policy — Open data in GeoLibre

Last updated: August 15, 2026
Last updated: August 16, 2026

Open data in GeoLibre does not independently collect, retain, or sell personal
information, browsing history, page contents, or usage analytics. It does
forward the complete dataset and style URLs that the user explicitly selects,
information, page contents, or usage analytics. It does forward the complete
dataset, service, and style URLs that the user explicitly selects,
as described below.

The extension uses Chrome's `activeTab` permission to inspect links and
structured metadata on the current page only after the user clicks the
extension's toolbar icon. Inspection happens locally in the browser. Results
are held in memory only while the popup is open.
extension's toolbar icon. The extension also observes completed HTTP(S)
requests locally to identify geospatial services used by interactive maps. It
does not inspect response bodies. Detected service URLs are held in Chrome's
in-memory session storage and removed when their tab closes.

When the user chooses **Open in GeoLibre**, the complete selected HTTP(S)
dataset and style URLs are placed in the query string of a new
Expand All @@ -23,7 +25,8 @@ The navigation request exposes its URL and the user's IP address to GeoLibre's
web-hosting infrastructure, where standard service logs may retain them. The
navigation may also appear in browser history. See the current
[GeoLibre privacy policy](https://geolibre.app/privacy/) for the service's data
practices. The extension itself does not retain the URLs after its popup closes.
practices. The extension itself does not persist URLs beyond the browser tab's
session.

The extension does not fetch or upload the datasets. GeoLibre requests them
directly from their original servers, subject to those servers' privacy
Expand All @@ -32,7 +35,7 @@ from the source page are not forwarded, although credentials embedded directly
in a selected URL are part of the URL and are forwarded.

The extension uses no remote code, advertising, analytics, tracking pixels,
cookies, accounts, or extension storage.
cookies, accounts, or persistent extension storage.

Questions may be submitted through the GeoLibre repository:
<https://github.com/opengeos/GeoLibre/issues>.
Loading
Loading