Skip to content

Commit

Permalink
Canonicalize tile url (#7594)
Browse files Browse the repository at this point in the history
* canonicalize tile urls so they use config.BASE_URL

* add tests for canonicalizing tile urls

* address review comments

* address kk's comments

* make tests more readable
  • Loading branch information
mollymerp authored Nov 26, 2018
1 parent fcaa869 commit c78fa7a
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 37 deletions.
14 changes: 14 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ const exported = {
set accessToken(token: string) {
config.ACCESS_TOKEN = token;
},
/**
* Gets and sets the map's default API URL for requesting tiles, styles, sprites, and glyphs
*
* @var {string} url
* @example
* mapboxgl.baseApiUrl = 'https://api.mapbox.com';
*/
get baseApiUrl(): ?string {
return config.API_URL;
},

set baseApiUrl(url: string) {
config.API_URL = url;
},

get workerCount(): number {
return WorkerPool.workerCount;
Expand Down
6 changes: 5 additions & 1 deletion src/source/load_tilejson.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { pick } from '../util/util';

import { getJSON, ResourceType } from '../util/ajax';
import browser from '../util/browser';
import { normalizeSourceURL as normalizeURL } from '../util/mapbox';
import { normalizeSourceURL as normalizeURL, canonicalizeTileset } from '../util/mapbox';

import type {RequestTransformFunction} from '../ui/map';
import type {Callback} from '../types/callback';
Expand All @@ -26,6 +26,10 @@ export default function(options: any, requestTransformFn: RequestTransformFuncti
result.vectorLayerIds = result.vectorLayers.map((layer) => { return layer.id; });
}

// only canonicalize tile tileset if source is declared using a tilejson url
if (options.url) {
result.tiles = canonicalizeTileset(result, options.url);
}
callback(null, result);
}
};
Expand Down
41 changes: 33 additions & 8 deletions src/util/mapbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { postData } from './ajax';

import type { RequestParameters } from './ajax';
import type { Cancelable } from '../types/cancelable';
import type {TileJSON} from '../types/tilejson';

const help = 'See https://www.mapbox.com/api-documentation/#access-tokens';
const telemEventKey = 'mapbox.eventData';
Expand Down Expand Up @@ -88,6 +89,8 @@ export const normalizeSpriteURL = function(url: string, format: string, extensio
};

const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/;
// matches any file extension specified by a dot and one or more alphanumeric characters
const extensionRe = /\.[\w]+$/;

export const normalizeTileURL = function(tileURL: string, sourceURL?: ?string, tileSize?: ?number): string {
if (!sourceURL || !isMapboxURL(sourceURL)) return tileURL;
Expand All @@ -100,18 +103,40 @@ export const normalizeTileURL = function(tileURL: string, sourceURL?: ?string, t
const suffix = browser.devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : '';
const extension = browser.supportsWebp ? '.webp' : '$1';
urlObject.path = urlObject.path.replace(imageExtensionRe, `${suffix}${extension}`);
urlObject.path = `/v4${urlObject.path}`;

replaceTempAccessToken(urlObject.params);
return formatUrl(urlObject);
return makeAPIURL(urlObject);
};

function replaceTempAccessToken(params: Array<string>) {
for (let i = 0; i < params.length; i++) {
if (params[i].indexOf('access_token=tk.') === 0) {
params[i] = `access_token=${config.ACCESS_TOKEN || ''}`;
}
export const canonicalizeTileURL = function(url: string) {
const version = "/v4/";

const urlObject = parseUrl(url);
// Make sure that we are dealing with a valid Mapbox tile URL.
// Has to begin with /v4/, with a valid filename + extension
if (!urlObject.path.match(/(^\/v4\/)/) || !urlObject.path.match(extensionRe)) {
// Not a proper Mapbox tile URL.
return url;
}
}
// Reassemble the canonical URL from the parts we've parsed before.
let result = "mapbox://tiles/";
result += urlObject.path.replace(version, '');

// Append the query string, minus the access token parameter.
const params = urlObject.params.filter(p => !p.match(/^access_token=/));
if (params.length) result += `?${params.join('&')}`;
return result;
};

export const canonicalizeTileset = function(tileJSON: TileJSON, sourceURL: string) {
if (!isMapboxURL(sourceURL)) return tileJSON.tiles || [];
const canonical = [];
for (const url of tileJSON.tiles) {
const canonicalUrl = canonicalizeTileURL(url);
canonical.push(canonicalUrl);
}
return canonical;
};

const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/;

Expand Down
126 changes: 98 additions & 28 deletions test/unit/util/mapbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ test("mapbox", (t) => {

t.beforeEach((callback) => {
config.ACCESS_TOKEN = 'key';
config.REQUIRE_ACCESS_TOKEN = true;
callback();
});

Expand Down Expand Up @@ -228,40 +229,105 @@ test("mapbox", (t) => {
t.end();
});

t.test('canonicalize raster tileset', (t) => {
const tileset = {tiles: ["http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=key"]};
mapbox.canonicalizeTileset(tileset, "mapbox://mapbox.satellite");
t.deepEquals(mapbox.canonicalizeTileset(tileset, "mapbox://mapbox.satellite"), ["mapbox://tiles/mapbox.satellite/{z}/{x}/{y}.png"]);
t.end();
});

t.test('canonicalize vector tileset', (t) => {
const tileset = {tiles: ["http://a.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.vector.pbf?access_token=key"]};
t.deepEquals(mapbox.canonicalizeTileset(tileset, "mapbox://mapbox.streets"), ["mapbox://tiles/mapbox.streets/{z}/{x}/{y}.vector.pbf"]);
t.end();
});

t.test('.canonicalizeTileURL', (t) => {
t.equals(mapbox.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("http://b.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("https://api.mapbox.cn/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b,c.d/{z}/{x}/{y}.vector.pbf?access_token=key"),
"mapbox://tiles/a.b,c.d/{z}/{x}/{y}.vector.pbf");
t.equals(mapbox.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key&custom=parameter"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter");
t.equals(mapbox.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter");
t.equals(mapbox.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key&second=param"),
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&second=param");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.jpg");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.jpg70");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.jpg");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.jpg70");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png"),
"mapbox://tiles/a.b/{z}/{x}/{y}.png");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.png");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png"),
"mapbox://tiles/a.b/{z}/{x}/{y}.png");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key"),
"mapbox://tiles/a.b/{z}/{x}/{y}.png");

// We don't ever expect to see these inputs, but be safe anyway.
t.equals(mapbox.canonicalizeTileURL("http://path"), "http://path");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/"), "http://api.mapbox.com/v4/");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}."), "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.");
t.equals(mapbox.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}/."), "http://api.mapbox.com/v4/a.b/{z}/{x}/{y}/.");
t.end();
});

t.test('.normalizeTileURL', (t) => {
browser.supportsWebp = false;

t.test('does nothing on 1x devices', (t) => {
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/tile.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/tile.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/tile.jpg70');
config.API_URL = 'http://path.png';
config.REQUIRE_ACCESS_TOKEN = false;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/v4/tile.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/v4/tile.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/v4/tile.jpg70');
t.end();
});

t.test('inserts @2x on 2x devices', (t) => {
window.devicePixelRatio = 2;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/tile@2x.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/tile@2x.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/tile@2x.jpg70');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), 'http://path.png/tile@2x.png?access_token=foo');
config.API_URL = 'http://path.png';
config.REQUIRE_ACCESS_TOKEN = false;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/v4/tile@2x.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/v4/tile@2x.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/v4/tile@2x.jpg70');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), 'http://path.png/v4/tile@2x.png?access_token=foo');
window.devicePixelRatio = 1;
t.end();
});

t.test('inserts @2x when tileSize == 512', (t) => {
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource, 512), 'http://path.png/tile@2x.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource, 512), 'http://path.png/tile@2x.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource, 512), 'http://path.png/tile@2x.jpg70');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource, 512), 'http://path.png/tile@2x.png?access_token=foo');
config.API_URL = 'http://path.png';
config.REQUIRE_ACCESS_TOKEN = false;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource, 512), 'http://path.png/v4/tile@2x.png');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource, 512), 'http://path.png/v4/tile@2x.png32');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource, 512), 'http://path.png/v4/tile@2x.jpg70');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource, 512), 'http://path.png/v4/tile@2x.png?access_token=foo');
t.end();
});

t.test('replaces img extension with webp on supporting devices', (t) => {
browser.supportsWebp = true;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), 'http://path.png/tile.webp?access_token=foo');
config.API_URL = 'http://path.png';
config.REQUIRE_ACCESS_TOKEN = false;
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png', mapboxSource), 'http://path.png/v4/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png32', mapboxSource), 'http://path.png/v4/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), 'http://path.png/v4/tile.webp');
t.equal(mapbox.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), 'http://path.png/v4/tile.webp?access_token=foo');
browser.supportsWebp = false;
t.end();
});
Expand All @@ -276,23 +342,12 @@ test("mapbox", (t) => {
t.end();
});

t.test('replace temp access tokens with the latest token', (t) => {
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?access_token=tk.abc.123', mapboxSource), 'http://example.com/tile.png?access_token=key');
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?foo=bar&access_token=tk.abc.123', mapboxSource), 'http://example.com/tile.png?foo=bar&access_token=key');
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?access_token=tk.abc.123&foo=bar', 'mapbox://user.map'), 'http://example.com/tile.png?access_token=key&foo=bar');
t.end();
});

t.test('does not modify the access token for non-mapbox sources', (t) => {
config.API_URL = 'http://example.com';
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?access_token=tk.abc.123', nonMapboxSource), 'http://example.com/tile.png?access_token=tk.abc.123');
t.end();
});

t.test('does not modify the access token for non temp tokens', (t) => {
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?access_token=pk.abc.123', mapboxSource), 'http://example.com/tile.png?access_token=pk.abc.123');
t.equal(mapbox.normalizeTileURL('http://example.com/tile.png?access_token=tkk.abc.123', mapboxSource), 'http://example.com/tile.png?access_token=tkk.abc.123');
t.end();
});

t.test('throw error on falsy url input', (t) => {
t.throws(() => {
Expand All @@ -301,8 +356,23 @@ test("mapbox", (t) => {
t.end();
});

browser.supportsWebp = true;
t.test('matches gl-native normalization', (t) => {
config.API_URL = 'https://api.mapbox.com/';
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf", mapboxSource), "https://api.mapbox.com/v4/a.b/0/0/0.pbf?access_token=key");
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0", mapboxSource), "https://api.mapbox.com/v4/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0&access_token=key");
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?", mapboxSource), "https://api.mapbox.com/v4/a.b/0/0/0.pbf?access_token=key");
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png", mapboxSource), "https://api.mapbox.com/v4/a.b/0/0/0.png?access_token=key");
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0@2x.png", mapboxSource), "https://api.mapbox.com/v4/a.b/0/0/0@2x.png?access_token=key");
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b,c.d/0/0/0.pbf", mapboxSource), "https://api.mapbox.com/v4/a.b,c.d/0/0/0.pbf?access_token=key");

config.API_URL = 'https://api.example.com/';
t.equal(mapbox.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png", mapboxSource), "https://api.example.com/v4/a.b/0/0/0.png?access_token=key");
t.equal(mapbox.normalizeTileURL("http://path", nonMapboxSource), "http://path");

t.end();
});

browser.supportsWebp = true;
t.end();
});

Expand Down

0 comments on commit c78fa7a

Please sign in to comment.