kopia lustrzana https://github.com/FacilMap/facilmap
Porównaj commity
2 Commity
b7eaa44324
...
4bfe3ff4c2
Autor | SHA1 | Data |
---|---|---|
Candid Dauth | 4bfe3ff4c2 | |
Candid Dauth | e8798f3cde |
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import Icon from "./ui/icon.vue";
|
||||
import type { FindPadsResult } from "facilmap-types";
|
||||
import decodeURIComponent from "decode-uri-component";
|
||||
import { computed, ref } from "vue";
|
||||
import { useToasts } from "./ui/toasts/toasts.vue";
|
||||
import Pagination from "./ui/pagination.vue";
|
||||
|
@ -11,6 +10,7 @@
|
|||
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import type { FacilMapContext } from "./facil-map-context-provider/facil-map-context";
|
||||
import ValidatedField from "./ui/validated-form/validated-field.vue";
|
||||
import { parsePadUrl } from "facilmap-utils";
|
||||
|
||||
const toasts = useToasts();
|
||||
|
||||
|
@ -20,15 +20,9 @@
|
|||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
function parsePadId(val: string, context: FacilMapContext): { padId: string; hash: string } {
|
||||
if (val.startsWith(context.baseUrl))
|
||||
val = decodeURIComponent(val.substr(context.baseUrl.length));
|
||||
|
||||
const hashIdx = val.indexOf("#");
|
||||
if (hashIdx == -1)
|
||||
return { padId: val, hash: "" };
|
||||
else
|
||||
return { padId: val.substr(0, hashIdx), hash: val.substr(hashIdx) };
|
||||
function parsePadId(val: string, context: FacilMapContext): { padId: string; hash: string } | undefined {
|
||||
const url = val.startsWith(context.baseUrl) ? val : `${context.baseUrl}${val}`;
|
||||
return parsePadUrl(url, context.baseUrl);
|
||||
}
|
||||
|
||||
const context = injectContextRequired();
|
||||
|
@ -50,18 +44,22 @@
|
|||
|
||||
const url = computed(() => {
|
||||
const parsed = parsePadId(padId.value, context);
|
||||
return context.baseUrl + encodeURIComponent(parsed.padId) + parsed.hash;
|
||||
if (parsed) {
|
||||
return context.baseUrl + encodeURIComponent(parsed.padId) + parsed.hash;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit(): void {
|
||||
const parsed = parsePadId(padId.value, context);
|
||||
client.value.openPad(parsed.padId);
|
||||
modalRef.value!.modal.hide();
|
||||
if (parsed) {
|
||||
client.value.openPad(parsed.padId);
|
||||
modalRef.value!.modal.hide();
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: This is called too early
|
||||
mapContext.value.components.hashHandler.applyHash(parsed.hash);
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
// TODO: This is called too early
|
||||
mapContext.value.components.hashHandler.applyHash(parsed.hash);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function openResult(result: FindPadsResult): void {
|
||||
|
@ -106,15 +104,19 @@
|
|||
function validatePadIdFormat(padId: string) {
|
||||
const parsed = parsePadId(padId, context);
|
||||
|
||||
if (parsed.padId.includes("/")) {
|
||||
if (!parsed) {
|
||||
return "Please enter a valid map ID or URL.";
|
||||
}
|
||||
}
|
||||
|
||||
async function validatePadExistence(padId: string) {
|
||||
const padInfo = await client.value.getPad({ padId });
|
||||
if (!padInfo) {
|
||||
return "No map with this ID could be found.";
|
||||
const parsed = parsePadId(padId, context);
|
||||
|
||||
if (parsed) {
|
||||
const padInfo = await client.value.getPad({ padId: parsed.padId });
|
||||
if (!padInfo) {
|
||||
return "No map with this ID could be found.";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<html class="fm-facilmap-map">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= padData && padData.name ? `${padData.name} – ` : ''%><%=appName%></title>
|
||||
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
|
||||
<title><%=normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, appName)%></title>
|
||||
<meta name="description" content="<%=normalizePageDescription(padData?.description)%>" />
|
||||
<% if(!padData || (isReadOnly && padData.searchEngines)) { -%>
|
||||
<meta name="robots" content="index,nofollow" />
|
||||
<% } else { -%>
|
||||
|
@ -16,6 +16,8 @@
|
|||
<link rel="apple-touch-icon" href="<%=paths.base%>static/app-180.png">
|
||||
<link rel="manifest" href="<%=paths.base%>manifest.json">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="<%=appName%>" href="<%=paths.base%>opensearch.xml">
|
||||
<link rel="alternate" type="application/json+oembed" href="<%=paths.base%>oembed?url=<%=encodeURIComponent(url)%>&format=json" title="<%=normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, appName)%>" />
|
||||
<link rel="iframely geolocation web-share resizable" href="<%=url%>">
|
||||
<style type="text/css">
|
||||
html, body, #app {
|
||||
margin: 0;
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<html class="fm-facilmap-table">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%=normalizePadName(padData.name)%> – <%=appName%></title>
|
||||
<title><%=normalizePageTitle(normalizePadName(padData.name), appName)%></title>
|
||||
<base href="../" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<%
|
||||
if(!padData || padData.searchEngines) {
|
||||
-%>
|
||||
<meta name="robots" content="index,nofollow" />
|
||||
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
|
||||
<meta name="description" content="<%=normalizePageDescription(padData.description)%>" />
|
||||
<%
|
||||
} else {
|
||||
-%>
|
||||
|
@ -21,6 +21,8 @@
|
|||
<link rel="mask-icon" href="<%=paths.base%>favicon.svg" color="#00272a">
|
||||
<link rel="apple-touch-icon" href="<%=paths.base%>static/app-180.png">
|
||||
<link rel="manifest" href="<%=paths.base%>static/manifest.json">
|
||||
<link rel="alternate" type="application/json+oembed" href="<%=paths.base%>oembed?url=<%=encodeURIComponent(url)%>&format=json" title="<%=normalizePageTitle(normalizePadName(padData.name), appName)%>" />
|
||||
<link rel="iframely resizable" href="<%=url%>">
|
||||
|
||||
<style type="text/css">
|
||||
<% for (const path of styles) { -%>
|
||||
|
|
|
@ -91,17 +91,22 @@ export function createSingleTable(
|
|||
})());
|
||||
}
|
||||
|
||||
export function createTable(database: Database, padId: PadId, filter: string | undefined, hide: string[]): ReadableStream<string> {
|
||||
export function createTable(database: Database, padId: PadId, filter: string | undefined, hide: string[], url: string): ReadableStream<string> {
|
||||
return streamPromiseToStream((async () => {
|
||||
const [padData, types] = await Promise.all([
|
||||
database.pads.getPadData(padId),
|
||||
asyncIteratorToArray(database.types.getTypes(padId))
|
||||
]);
|
||||
|
||||
if (!padData) {
|
||||
throw new Error(`Pad with read-only ID ${padId} could not be found.`);
|
||||
}
|
||||
|
||||
return renderTable({
|
||||
padData,
|
||||
types,
|
||||
renderSingleTable: (typeId, params) => createSingleTable(database, padId, typeId, filter, hide, params)
|
||||
renderSingleTable: (typeId, params) => createSingleTable(database, padId, typeId, filter, hide, params),
|
||||
url
|
||||
});
|
||||
})());
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { ID, PadData, Type } from "facilmap-types";
|
|||
import * as ejs from "ejs";
|
||||
import { Router, type RequestHandler } from "express";
|
||||
import { static as expressStatic } from "express";
|
||||
import { normalizePadName, type InjectedConfig, quoteHtml } from "facilmap-utils";
|
||||
import { normalizePadName, type InjectedConfig, quoteHtml, normalizePageTitle, normalizePageDescription } from "facilmap-utils";
|
||||
import config from "./config";
|
||||
import { streamPromiseToStream, streamReplace } from "./utils/streams";
|
||||
import { ReadableStream } from "stream/web";
|
||||
|
@ -70,6 +70,7 @@ function getInjectedConfig(): InjectedConfig {
|
|||
export interface RenderMapParams {
|
||||
padData: Pick<PadData, "name" | "description" | "searchEngines"> | undefined;
|
||||
isReadOnly: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export async function renderMap(params: RenderMapParams): Promise<string> {
|
||||
|
@ -82,16 +83,20 @@ export async function renderMap(params: RenderMapParams): Promise<string> {
|
|||
appName: config.appName,
|
||||
config: getInjectedConfig(),
|
||||
hasCustomCssFile: !!config.customCssFile,
|
||||
normalizePageTitle,
|
||||
normalizePageDescription,
|
||||
normalizePadName,
|
||||
...injections,
|
||||
paths,
|
||||
...params
|
||||
});
|
||||
}
|
||||
|
||||
export function renderTable({ padData, types, renderSingleTable }: {
|
||||
padData: PadData | undefined;
|
||||
export function renderTable({ padData, types, renderSingleTable, url }: {
|
||||
padData: PadData;
|
||||
types: Type[];
|
||||
renderSingleTable: (typeId: ID, params: TableParams) => ReadableStream<string>;
|
||||
url: string;
|
||||
}): ReadableStream<string> {
|
||||
return streamPromiseToStream((async () => {
|
||||
const [template, injections] = await Promise.all([
|
||||
|
@ -106,6 +111,8 @@ export function renderTable({ padData, types, renderSingleTable }: {
|
|||
hasCustomCssFile: !!config.customCssFile,
|
||||
paths,
|
||||
normalizePadName,
|
||||
normalizePageTitle,
|
||||
normalizePageDescription,
|
||||
quoteHtml,
|
||||
renderSingleTable: (typeId: ID, params: TableParams) => {
|
||||
const placeholder = `%${generateRandomId(32)}%`;
|
||||
|
@ -114,6 +121,7 @@ export function renderTable({ padData, types, renderSingleTable }: {
|
|||
},
|
||||
padData,
|
||||
types,
|
||||
url
|
||||
});
|
||||
|
||||
return streamReplace(rendered, replace);
|
||||
|
@ -153,4 +161,20 @@ export async function getOpensearchXml(baseUrl: string): Promise<string> {
|
|||
appName: config.appName,
|
||||
baseUrl
|
||||
});
|
||||
}
|
||||
|
||||
export function getOembedJson(baseUrl: string, padData: PadData | undefined, params: { url: string; maxwidth?: number; maxheight?: number }): string {
|
||||
const width = params.maxwidth ?? 800;
|
||||
const height = params.maxheight ?? 500;
|
||||
|
||||
return JSON.stringify({
|
||||
"title": normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, config.appName),
|
||||
"type": "rich",
|
||||
"height": height,
|
||||
"width": width,
|
||||
"version": "1.0",
|
||||
"provider_name": config.appName,
|
||||
"provider_url": baseUrl,
|
||||
"html": `<iframe style="height:${height}px; width:${width}px; border:none;" src="${quoteHtml(params.url)}"></iframe>`
|
||||
});
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import compression from "compression";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import { createServer, type Server as HttpServer } from "http";
|
||||
import { stringifiedIdValidator, type PadId } from "facilmap-types";
|
||||
import { stringifiedIdValidator, type PadData, type PadId } from "facilmap-types";
|
||||
import { createSingleTable, createTable } from "./export/table.js";
|
||||
import Database from "./database/database";
|
||||
import { exportGeoJson } from "./export/geojson.js";
|
||||
import { exportGpx, exportGpxZip } from "./export/gpx.js";
|
||||
import domainMiddleware from "express-domain-middleware";
|
||||
import { Readable, Writable } from "stream";
|
||||
import { getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
|
||||
import { getSafeFilename, normalizePadName } from "facilmap-utils";
|
||||
import { getOembedJson, getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
|
||||
import { getSafeFilename, normalizePadName, parsePadUrl } from "facilmap-utils";
|
||||
import { paths } from "facilmap-frontend/build.js";
|
||||
import config from "./config";
|
||||
import { exportCsv } from "./export/csv.js";
|
||||
|
@ -21,6 +21,8 @@ function getBaseUrl(req: Request): string {
|
|||
|
||||
export async function initWebserver(database: Database, port: number, host?: string): Promise<HttpServer> {
|
||||
const padMiddleware = async (req: Request<{ padId: string }>, res: Response<string>) => {
|
||||
const baseUrl = getBaseUrl(req);
|
||||
|
||||
let params: RenderMapParams;
|
||||
if(req.params?.padId) {
|
||||
const padData = await database.pads.getPadDataByAnyId(req.params.padId);
|
||||
|
@ -31,7 +33,8 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
name: normalizePadName(padData.name),
|
||||
description: padData.description
|
||||
},
|
||||
isReadOnly: padData.id === req.params.padId
|
||||
isReadOnly: padData.id === req.params.padId,
|
||||
url: `${baseUrl}${encodeURIComponent(req.params.padId)}`
|
||||
};
|
||||
} else {
|
||||
res.status(404);
|
||||
|
@ -41,13 +44,15 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
name: "",
|
||||
description: ""
|
||||
},
|
||||
isReadOnly: true
|
||||
isReadOnly: true,
|
||||
url: `${baseUrl}${encodeURIComponent(req.params.padId)}`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
params = {
|
||||
padData: undefined,
|
||||
isReadOnly: true
|
||||
isReadOnly: true,
|
||||
url: baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -83,10 +88,43 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
}
|
||||
});
|
||||
|
||||
app.use("/_app/static/sw.js", (req, res, next) => {
|
||||
app.use(`${paths.base}static/sw.js`, (req, res, next) => {
|
||||
res.setHeader("Service-Worker-Allowed", "/");
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(`${paths.base}oembed`, async (req, res, next) => {
|
||||
const query = z.object({
|
||||
url: z.string(),
|
||||
maxwidth: z.number().optional(),
|
||||
maxheight: z.number().optional(),
|
||||
format: z.string().optional()
|
||||
}).parse(req.query);
|
||||
|
||||
if (query.format != null && query.format !== "json") {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl(req);
|
||||
let padData: PadData | undefined;
|
||||
if (query.url === baseUrl || `${query.url}/` === baseUrl) {
|
||||
padData = undefined;
|
||||
} else {
|
||||
const parsed = parsePadUrl(query.url, baseUrl);
|
||||
if (parsed) {
|
||||
padData = await database.pads.getPadDataByAnyId(parsed.padId);
|
||||
} else {
|
||||
res.status(404).send();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.header("Content-type", "application/json");
|
||||
|
||||
res.send(getOembedJson(baseUrl, padData, query));
|
||||
});
|
||||
|
||||
app.use(await getStaticFrontendMiddleware());
|
||||
|
||||
// If no file with this name has been found, we render a pad
|
||||
|
@ -129,6 +167,7 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
filter: z.string().optional(),
|
||||
hide: z.string().optional()
|
||||
}).parse(req.query);
|
||||
const baseUrl = getBaseUrl(req);
|
||||
|
||||
res.type("html");
|
||||
res.setHeader("Referrer-Policy", "origin");
|
||||
|
@ -136,7 +175,8 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
database,
|
||||
req.params.padId,
|
||||
query.filter,
|
||||
query.hide ? query.hide.split(',') : []
|
||||
query.hide ? query.hide.split(',') : [],
|
||||
`${baseUrl}${encodeURIComponent(req.params.padId)}/table`
|
||||
).pipeTo(Writable.toWeb(res));
|
||||
});
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"decode-uri-component": "^0.4.1",
|
||||
"domhandler": "^5.0.3",
|
||||
"dompurify": "^3.0.9",
|
||||
"facilmap-types": "workspace:^",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { cloneDeep, isEqual } from "lodash-es";
|
||||
import decodeURIComponent from "decode-uri-component";
|
||||
|
||||
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const LENGTH = 12;
|
||||
|
@ -96,8 +97,10 @@ export function getObjectDiff(obj1: Record<keyof any, any>, obj2: Record<keyof a
|
|||
export function decodeQueryString(str: string): Record<string, string> {
|
||||
const obj: Record<string, string> = { };
|
||||
for(const segment of str.replace(/^\?/, "").split(/[;&]/)) {
|
||||
const pair = segment.split("=");
|
||||
obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
|
||||
if (segment !== "") {
|
||||
const pair = segment.split("=");
|
||||
obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] ?? "");
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
@ -146,6 +149,14 @@ export function normalizeLineName(name: string | undefined): string {
|
|||
return name || "Untitled line";
|
||||
}
|
||||
|
||||
export function normalizePageTitle(padName: string | undefined, appName: string): string {
|
||||
return `${padName ? `${padName} – ` : ''}${appName}`;
|
||||
}
|
||||
|
||||
export function normalizePageDescription(padDescription: string | undefined): string {
|
||||
return padDescription || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
|
||||
* @param oldObject {Object}
|
||||
|
@ -166,4 +177,18 @@ export function mergeObject<T extends Record<keyof any, any>>(oldObject: T | und
|
|||
|
||||
export function getSafeFilename(fname: string): string {
|
||||
return fname.replace(/[\\/:*?"<>|]+/g, '_');
|
||||
}
|
||||
|
||||
export function parsePadUrl(url: string, baseUrl: string): { padId: string; hash: string } | undefined {
|
||||
if (url.startsWith(baseUrl)) {
|
||||
const m = url.slice(baseUrl.length).match(/^([^/]+)(\/table)?(\?|#|$)/);
|
||||
|
||||
if (m) {
|
||||
const hashIdx = url.indexOf("#");
|
||||
return {
|
||||
padId: decodeURIComponent(m[1]),
|
||||
hash: hashIdx === -1 ? "" : url.substr(hashIdx)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3846,6 +3846,7 @@ __metadata:
|
|||
"@types/jsdom": ^21.1.6
|
||||
"@types/linkifyjs": ^2.1.7
|
||||
cheerio: ^1.0.0-rc.12
|
||||
decode-uri-component: ^0.4.1
|
||||
domhandler: ^5.0.3
|
||||
dompurify: ^3.0.9
|
||||
facilmap-types: "workspace:^"
|
||||
|
|
Ładowanie…
Reference in New Issue