Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/lib/geolonia-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict';

import assert from 'assert';
import {
transformGeoloniaApiSource,
transformGeoloniaTileSource,
transformGeoloniaSprite,
} from './transform-request';

describe('transformGeoloniaApiSource', () => {
const sourcesUrl = new URL('https://api.geolonia.com/sources?key=test-key&sessionId=abc123');

it('should redirect api.geolonia.com URLs to sourcesUrl', () => {
const result = transformGeoloniaApiSource(
'https://api.geolonia.com/some/path',
sourcesUrl,
);
assert.deepStrictEqual(result, { url: sourcesUrl.toString() });
});

it('should return null for non-Geolonia URLs', () => {
const result = transformGeoloniaApiSource(
'https://example.com/tiles',
sourcesUrl,
);
assert.strictEqual(result, null);
});
});

describe('transformGeoloniaTileSource', () => {
const atts = { key: 'test-key', stage: 'v1' };
const sessionId = 'session123';

it('should transform geolonia:// tile URLs and inject key/sessionId', () => {
const result = transformGeoloniaTileSource(
'geolonia://tiles/myuser/my-tileset',
atts,
sessionId,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.hostname, 'tileserver.geolonia.com');
assert.strictEqual(url.pathname, '/customtiles/my-tileset/tiles.json');
assert.strictEqual(url.searchParams.get('key'), 'test-key');
assert.strictEqual(url.searchParams.get('sessionId'), 'session123');
});

it('should inject key/sessionId into Geolonia tiles host URLs', () => {
const result = transformGeoloniaTileSource(
'https://tileserver.geolonia.com/some/path',
atts,
sessionId,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.searchParams.get('key'), 'test-key');
assert.strictEqual(url.searchParams.get('sessionId'), 'session123');
});

it('should switch to dev hostname when stage is dev', () => {
const devAtts = { key: 'test-key', stage: 'dev' };
const result = transformGeoloniaTileSource(
'https://tileserver.geolonia.com/some/path',
devAtts,
sessionId,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.hostname, 'tileserver-dev.geolonia.com');
});

it('should return null for non-Geolonia URLs', () => {
const result = transformGeoloniaTileSource(
'https://example.com/tiles',
atts,
sessionId,
);
assert.strictEqual(result, null);
});

it('should handle *.tiles.geolonia.com hosts', () => {
const result = transformGeoloniaTileSource(
'https://foo.tiles.geolonia.com/path',
atts,
sessionId,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.searchParams.get('key'), 'test-key');
});
});

describe('transformGeoloniaSprite', () => {
const atts = { key: 'test-key', stage: 'v1' };

it('should inject key and correct stage into Geolonia sprite URLs', () => {
const result = transformGeoloniaSprite(
'https://api.geolonia.com/dev/sprites/basic-v2/sprite',
atts,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.pathname, '/v1/sprites/basic-v2/sprite');
assert.strictEqual(url.searchParams.get('key'), 'test-key');
});

it('should handle v1 stage in URL', () => {
const result = transformGeoloniaSprite(
'https://api.geolonia.com/v1/sprites/basic-v2/sprite',
atts,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.pathname, '/v1/sprites/basic-v2/sprite');
assert.strictEqual(url.searchParams.get('key'), 'test-key');
});

it('should return null for non-Geolonia sprite URLs', () => {
const result = transformGeoloniaSprite(
'https://example.com/sprites/basic/sprite.json',
atts,
);
assert.strictEqual(result, null);
});

it('should switch stage from v1 to dev when atts.stage is dev', () => {
const devAtts = { key: 'test-key', stage: 'dev' };
const result = transformGeoloniaSprite(
'https://api.geolonia.com/v1/sprites/basic-v2/sprite',
devAtts,
);
assert.ok(result);
const url = new URL(result.url);
assert.strictEqual(url.pathname, '/dev/sprites/basic-v2/sprite');
});
});
71 changes: 20 additions & 51 deletions src/lib/geolonia-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ import {
handleErrorMode,
loadImageCompatibility,
GetImageCallback,
isGeoloniaTilesHost,
} from './util';
import {
transformGeoloniaApiSource,
transformGeoloniaTileSource,
transformGeoloniaSprite,
} from './transform-request';

export type GeoloniaMapOptions = MapOptions & { interactive?: boolean };

Expand Down Expand Up @@ -126,63 +130,28 @@ export default class GeoloniaMap extends maplibregl.Map {
// Pass API key and requested tile version to `/sources` (tile json).
const _transformRequest = options.transformRequest;
options.transformRequest = (url, resourceType) => {
if (
resourceType === 'Source' &&
url.startsWith('https://api.geolonia.com')
) {
return {
url: sourcesUrl.toString(),
};
}

let transformedUrl = url;
if (url.startsWith('geolonia://')) {
const tilesMatch = url.match(
/^geolonia:\/\/tiles\/(?<username>.+)\/(?<customtileId>.+)/,
if (resourceType === 'Source') {
return (
transformGeoloniaApiSource(url, sourcesUrl) ??
transformGeoloniaTileSource(url, atts, sessionId) ??
(typeof _transformRequest === 'function'
? _transformRequest(url, resourceType)
: undefined)
);
if (tilesMatch) {
transformedUrl = `https://tileserver.geolonia.com/customtiles/${tilesMatch.groups.customtileId}/tiles.json`;
}
}

const transformedUrlObj = new URL(transformedUrl);
const geoloniaTilesHost = isGeoloniaTilesHost(transformedUrlObj);

// Geolonia ドメインのみに API キーを付与(外部 URL には付与しない)
if (
resourceType === 'Source' &&
geoloniaTilesHost
) {
if (atts.stage === 'dev') {
transformedUrlObj.hostname = 'tileserver-dev.geolonia.com';
}
transformedUrlObj.searchParams.set('sessionId', sessionId);
transformedUrlObj.searchParams.set('key', atts.key);
return {
url: transformedUrlObj.toString(),
};
} else if (
(resourceType === 'SpriteJSON' || resourceType === 'SpriteImage') &&
transformedUrl.match(
/^https:\/\/api\.geolonia\.com\/(dev|v1)\/sprites\//,
)
) {
const pathParts = transformedUrlObj.pathname.split('/');
pathParts[1] = String(atts.stage);
transformedUrlObj.pathname = pathParts.join('/');
transformedUrlObj.searchParams.set('key', atts.key);
return {
url: transformedUrlObj.toString(),
};
if (resourceType === 'SpriteJSON' || resourceType === 'SpriteImage') {
return (
transformGeoloniaSprite(url, atts) ??
(typeof _transformRequest === 'function'
? _transformRequest(url, resourceType)
: undefined)
);
}

let request;
// Additional transformation
if (typeof _transformRequest === 'function') {
request = _transformRequest(transformedUrl, resourceType);
return _transformRequest(url, resourceType);
}

return request;
};

try {
Expand Down
61 changes: 61 additions & 0 deletions src/lib/transform-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { isGeoloniaTilesHost } from './util';

type RequestParameters = { url: string };
type TransformAtts = { key: string; stage: string };

/** Redirect Geolonia API source requests (style JSON sources) to the sourcesUrl */
export function transformGeoloniaApiSource(
url: string,
sourcesUrl: URL,
): RequestParameters | null {
if (url.startsWith('https://api.geolonia.com')) {
return { url: sourcesUrl.toString() };
}
return null;
}

/** Transform geolonia:// scheme and inject key/sessionId into Geolonia tile server sources */
export function transformGeoloniaTileSource(
url: string,
atts: TransformAtts,
sessionId: string,
): RequestParameters | null {
let transformedUrl = url;

if (url.startsWith('geolonia://')) {
const tilesMatch = url.match(
/^geolonia:\/\/tiles\/(?<username>.+)\/(?<customtileId>.+)/,
);
if (tilesMatch) {
transformedUrl = `https://tileserver.geolonia.com/customtiles/${tilesMatch.groups.customtileId}/tiles.json`;
}
}
Comment on lines +25 to +32
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

geolonia:// URLがパターンにマッチしない場合にランタイムエラーが発生する可能性があります。

URLが geolonia:// で始まるが、tilesMatch のパターン(geolonia://tiles/username/customtileId)にマッチしない場合、transformedUrl は変換されずに geolonia://... のまま残ります。その後、Line 34 で new URL(transformedUrl) を呼び出すと、geolonia:// は有効なURLスキームではないため、TypeError がスローされます。

🐛 修正案: マッチしない場合は早期リターン
   if (url.startsWith('geolonia://')) {
     const tilesMatch = url.match(
       /^geolonia:\/\/tiles\/(?<username>.+)\/(?<customtileId>.+)/,
     );
     if (tilesMatch) {
       transformedUrl = `https://tileserver.geolonia.com/customtiles/${tilesMatch.groups.customtileId}/tiles.json`;
+    } else {
+      // geolonia:// URLがパターンにマッチしない場合は変換をスキップ
+      return null;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (url.startsWith('geolonia://')) {
const tilesMatch = url.match(
/^geolonia:\/\/tiles\/(?<username>.+)\/(?<customtileId>.+)/,
);
if (tilesMatch) {
transformedUrl = `https://tileserver.geolonia.com/customtiles/${tilesMatch.groups.customtileId}/tiles.json`;
}
}
if (url.startsWith('geolonia://')) {
const tilesMatch = url.match(
/^geolonia:\/\/tiles\/(?<username>.+)\/(?<customtileId>.+)/,
);
if (tilesMatch) {
transformedUrl = `https://tileserver.geolonia.com/customtiles/${tilesMatch.groups.customtileId}/tiles.json`;
} else {
// geolonia:// URLがパターンにマッチしない場合は変換をスキップ
return null;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/transform-request.ts` around lines 25 - 32, The code currently leaves
transformedUrl as the original geolonia:// string when a geolonia:// URL doesn't
match the tiles pattern, causing new URL(transformedUrl) to throw; update the
geolonia:// handling in transform-request.ts (the block that uses
url.startsWith('geolonia://') and tilesMatch) to return early or set a safe
transformedUrl when tilesMatch is falsy (e.g. reject/throw a controlled error or
return the original Request/Response) so that new URL(...) is never called with
an unsupported scheme; ensure you reference the tilesMatch check and the
transformedUrl variable when making the change.


const transformedUrlObj = new URL(transformedUrl);
if (!isGeoloniaTilesHost(transformedUrlObj)) {
return null;
}

if (atts.stage === 'dev') {
transformedUrlObj.hostname = 'tileserver-dev.geolonia.com';
}
transformedUrlObj.searchParams.set('sessionId', sessionId);
transformedUrlObj.searchParams.set('key', atts.key);
return { url: transformedUrlObj.toString() };
}

/** Inject key and correct stage into Geolonia sprite requests */
export function transformGeoloniaSprite(
url: string,
atts: TransformAtts,
): RequestParameters | null {
if (!url.match(/^https:\/\/api\.geolonia\.com\/(dev|v1)\/sprites\//)) {
return null;
}
const urlObj = new URL(url);
const pathParts = urlObj.pathname.split('/');
pathParts[1] = String(atts.stage);
urlObj.pathname = pathParts.join('/');
urlObj.searchParams.set('key', atts.key);
return { url: urlObj.toString() };
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type EmbedAttributes = {
plugin: string;
key: string;
apiUrl: string;
stage: string;
loader: string;
minZoom: string | number;
maxZoom: string | number;
Expand Down