Spaces:
Sleeping
Sleeping
| // @flow | |
| /***** START WARNING - IF YOU USE THIS CODE WITH MAPBOX MAPPING APIS, REMOVAL OR | |
| * MODIFICATION OF THE FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ****** | |
| * The following code is used to access Mapbox's Mapping APIs. Removal or modification | |
| * of this code when used with Mapbox's Mapping APIs can result in higher fees and/or | |
| * termination of your account with Mapbox. | |
| * | |
| * Under the Mapbox Terms of Service, you may not use this code to access Mapbox | |
| * Mapping APIs other than through Mapbox SDKs. | |
| * | |
| * The Mapping APIs documentation is available at https://docs.mapbox.com/api/maps/#maps | |
| * and the Mapbox Terms of Service are available at https://www.mapbox.com/tos/ | |
| ******************************************************************************/ | |
| import config from './config'; | |
| import browser from './browser'; | |
| import window from './window'; | |
| import webpSupported from './webp_supported'; | |
| import {createSkuToken, SKU_ID} from './sku_token'; | |
| import {version as sdkVersion} from '../../package.json'; | |
| import {uuid, validateUuid, storageAvailable, b64DecodeUnicode, b64EncodeUnicode, warnOnce, extend} from './util'; | |
| import {postData, ResourceType} from './ajax'; | |
| import type {RequestParameters} from './ajax'; | |
| import type {Cancelable} from '../types/cancelable'; | |
| import type {TileJSON} from '../types/tilejson'; | |
| type ResourceTypeEnum = $Keys<typeof ResourceType>; | |
| export type RequestTransformFunction = (url: string, resourceType?: ResourceTypeEnum) => RequestParameters; | |
| type UrlObject = {| | |
| protocol: string, | |
| authority: string, | |
| path: string, | |
| params: Array<string> | |
| |}; | |
| export class RequestManager { | |
| _skuToken: string; | |
| _skuTokenExpiresAt: number; | |
| _transformRequestFn: ?RequestTransformFunction; | |
| _customAccessToken: ?string; | |
| constructor(transformRequestFn?: RequestTransformFunction, customAccessToken?: string) { | |
| this._transformRequestFn = transformRequestFn; | |
| this._customAccessToken = customAccessToken; | |
| this._createSkuToken(); | |
| } | |
| _createSkuToken() { | |
| const skuToken = createSkuToken(); | |
| this._skuToken = skuToken.token; | |
| this._skuTokenExpiresAt = skuToken.tokenExpiresAt; | |
| } | |
| _isSkuTokenExpired(): boolean { | |
| return Date.now() > this._skuTokenExpiresAt; | |
| } | |
| transformRequest(url: string, type: ResourceTypeEnum) { | |
| if (this._transformRequestFn) { | |
| return this._transformRequestFn(url, type) || {url}; | |
| } | |
| return {url}; | |
| } | |
| normalizeStyleURL(url: string, accessToken?: string): string { | |
| if (!isMapboxURL(url)) return url; | |
| const urlObject = parseUrl(url); | |
| urlObject.path = `/styles/v1${urlObject.path}`; | |
| return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); | |
| } | |
| normalizeGlyphsURL(url: string, accessToken?: string): string { | |
| if (!isMapboxURL(url)) return url; | |
| const urlObject = parseUrl(url); | |
| urlObject.path = `/fonts/v1${urlObject.path}`; | |
| return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); | |
| } | |
| normalizeSourceURL(url: string, accessToken?: string): string { | |
| if (!isMapboxURL(url)) return url; | |
| const urlObject = parseUrl(url); | |
| urlObject.path = `/v4/${urlObject.authority}.json`; | |
| // TileJSON requests need a secure flag appended to their URLs so | |
| // that the server knows to send SSL-ified resource references. | |
| urlObject.params.push('secure'); | |
| return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); | |
| } | |
| normalizeSpriteURL(url: string, format: string, extension: string, accessToken?: string): string { | |
| const urlObject = parseUrl(url); | |
| if (!isMapboxURL(url)) { | |
| urlObject.path += `${format}${extension}`; | |
| return formatUrl(urlObject); | |
| } | |
| urlObject.path = `/styles/v1${urlObject.path}/sprite${format}${extension}`; | |
| return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); | |
| } | |
| normalizeTileURL(tileURL: string, tileSize?: ?number): string { | |
| if (this._isSkuTokenExpired()) { | |
| this._createSkuToken(); | |
| } | |
| if (tileURL && !isMapboxURL(tileURL)) return tileURL; | |
| const urlObject = parseUrl(tileURL); | |
| const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/; | |
| const tileURLAPIPrefixRe = /^.+\/v4\//; | |
| // The v4 mapbox tile API supports 512x512 image tiles only when @2x | |
| // is appended to the tile URL. If `tileSize: 512` is specified for | |
| // a Mapbox raster source force the @2x suffix even if a non hidpi device. | |
| const suffix = browser.devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : ''; | |
| const extension = webpSupported.supported ? '.webp' : '$1'; | |
| urlObject.path = urlObject.path.replace(imageExtensionRe, `${suffix}${extension}`); | |
| urlObject.path = urlObject.path.replace(tileURLAPIPrefixRe, '/'); | |
| urlObject.path = `/v4${urlObject.path}`; | |
| const accessToken = this._customAccessToken || getAccessToken(urlObject.params) || config.ACCESS_TOKEN; | |
| if (config.REQUIRE_ACCESS_TOKEN && accessToken && this._skuToken) { | |
| urlObject.params.push(`sku=${this._skuToken}`); | |
| } | |
| return this._makeAPIURL(urlObject, accessToken); | |
| } | |
| canonicalizeTileURL(url: string, removeAccessToken: boolean) { | |
| const version = "/v4/"; | |
| // matches any file extension specified by a dot and one or more alphanumeric characters | |
| const extensionRe = /\.[\w]+$/; | |
| 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. | |
| let params = urlObject.params; | |
| if (removeAccessToken) { | |
| params = params.filter(p => !p.match(/^access_token=/)); | |
| } | |
| if (params.length) result += `?${params.join('&')}`; | |
| return result; | |
| } | |
| canonicalizeTileset(tileJSON: TileJSON, sourceURL?: string) { | |
| const removeAccessToken = sourceURL ? isMapboxURL(sourceURL) : false; | |
| const canonical = []; | |
| for (const url of tileJSON.tiles || []) { | |
| if (isMapboxHTTPURL(url)) { | |
| canonical.push(this.canonicalizeTileURL(url, removeAccessToken)); | |
| } else { | |
| canonical.push(url); | |
| } | |
| } | |
| return canonical; | |
| } | |
| _makeAPIURL(urlObject: UrlObject, accessToken: string | null | void): string { | |
| const help = 'See https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes'; | |
| const apiUrlObject = parseUrl(config.API_URL); | |
| urlObject.protocol = apiUrlObject.protocol; | |
| urlObject.authority = apiUrlObject.authority; | |
| if (urlObject.protocol === 'http') { | |
| const i = urlObject.params.indexOf('secure'); | |
| if (i >= 0) urlObject.params.splice(i, 1); | |
| } | |
| if (apiUrlObject.path !== '/') { | |
| urlObject.path = `${apiUrlObject.path}${urlObject.path}`; | |
| } | |
| if (!config.REQUIRE_ACCESS_TOKEN) return formatUrl(urlObject); | |
| accessToken = accessToken || config.ACCESS_TOKEN; | |
| if (!accessToken) | |
| throw new Error(`An API access token is required to use Mapbox GL. ${help}`); | |
| if (accessToken[0] === 's') | |
| throw new Error(`Use a public access token (pk.*) with Mapbox GL, not a secret access token (sk.*). ${help}`); | |
| urlObject.params = urlObject.params.filter((d) => d.indexOf('access_token') === -1); | |
| urlObject.params.push(`access_token=${accessToken}`); | |
| return formatUrl(urlObject); | |
| } | |
| } | |
| function isMapboxURL(url: string) { | |
| return url.indexOf('mapbox:') === 0; | |
| } | |
| const mapboxHTTPURLRe = /^((https?:)?\/\/)?([^\/]+\.)?mapbox\.c(n|om)(\/|\?|$)/i; | |
| function isMapboxHTTPURL(url: string): boolean { | |
| return mapboxHTTPURLRe.test(url); | |
| } | |
| function hasCacheDefeatingSku(url: string) { | |
| return url.indexOf('sku=') > 0 && isMapboxHTTPURL(url); | |
| } | |
| function getAccessToken(params: Array<string>): string | null { | |
| for (const param of params) { | |
| const match = param.match(/^access_token=(.*)$/); | |
| if (match) { | |
| return match[1]; | |
| } | |
| } | |
| return null; | |
| } | |
| const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/; | |
| function parseUrl(url: string): UrlObject { | |
| const parts = url.match(urlRe); | |
| if (!parts) { | |
| throw new Error('Unable to parse URL object'); | |
| } | |
| return { | |
| protocol: parts[1], | |
| authority: parts[2], | |
| path: parts[3] || '/', | |
| params: parts[4] ? parts[4].split('&') : [] | |
| }; | |
| } | |
| function formatUrl(obj: UrlObject): string { | |
| const params = obj.params.length ? `?${obj.params.join('&')}` : ''; | |
| return `${obj.protocol}://${obj.authority}${obj.path}${params}`; | |
| } | |
| export {isMapboxURL, isMapboxHTTPURL, hasCacheDefeatingSku}; | |
| const telemEventKey = 'mapbox.eventData'; | |
| function parseAccessToken(accessToken: ?string) { | |
| if (!accessToken) { | |
| return null; | |
| } | |
| const parts = accessToken.split('.'); | |
| if (!parts || parts.length !== 3) { | |
| return null; | |
| } | |
| try { | |
| const jsonData = JSON.parse(b64DecodeUnicode(parts[1])); | |
| return jsonData; | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| type TelemetryEventType = 'appUserTurnstile' | 'map.load'; | |
| class TelemetryEvent { | |
| eventData: any; | |
| anonId: ?string; | |
| queue: Array<any>; | |
| type: TelemetryEventType; | |
| pendingRequest: ?Cancelable; | |
| _customAccessToken: ?string; | |
| constructor(type: TelemetryEventType) { | |
| this.type = type; | |
| this.anonId = null; | |
| this.eventData = {}; | |
| this.queue = []; | |
| this.pendingRequest = null; | |
| } | |
| getStorageKey(domain: ?string) { | |
| const tokenData = parseAccessToken(config.ACCESS_TOKEN); | |
| let u = ''; | |
| if (tokenData && tokenData['u']) { | |
| u = b64EncodeUnicode(tokenData['u']); | |
| } else { | |
| u = config.ACCESS_TOKEN || ''; | |
| } | |
| return domain ? | |
| `${telemEventKey}.${domain}:${u}` : | |
| `${telemEventKey}:${u}`; | |
| } | |
| fetchEventData() { | |
| const isLocalStorageAvailable = storageAvailable('localStorage'); | |
| const storageKey = this.getStorageKey(); | |
| const uuidKey = this.getStorageKey('uuid'); | |
| if (isLocalStorageAvailable) { | |
| //Retrieve cached data | |
| try { | |
| const data = window.localStorage.getItem(storageKey); | |
| if (data) { | |
| this.eventData = JSON.parse(data); | |
| } | |
| const uuid = window.localStorage.getItem(uuidKey); | |
| if (uuid) this.anonId = uuid; | |
| } catch (e) { | |
| warnOnce('Unable to read from LocalStorage'); | |
| } | |
| } | |
| } | |
| saveEventData() { | |
| const isLocalStorageAvailable = storageAvailable('localStorage'); | |
| const storageKey = this.getStorageKey(); | |
| const uuidKey = this.getStorageKey('uuid'); | |
| if (isLocalStorageAvailable) { | |
| try { | |
| window.localStorage.setItem(uuidKey, this.anonId); | |
| if (Object.keys(this.eventData).length >= 1) { | |
| window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); | |
| } | |
| } catch (e) { | |
| warnOnce('Unable to write to LocalStorage'); | |
| } | |
| } | |
| } | |
| processRequests(_: ?string) {} | |
| /* | |
| * If any event data should be persisted after the POST request, the callback should modify eventData` | |
| * to the values that should be saved. For this reason, the callback should be invoked prior to the call | |
| * to TelemetryEvent#saveData | |
| */ | |
| postEvent(timestamp: number, additionalPayload: {[_: string]: any}, callback: (err: ?Error) => void, customAccessToken?: ?string) { | |
| if (!config.EVENTS_URL) return; | |
| const eventsUrlObject: UrlObject = parseUrl(config.EVENTS_URL); | |
| eventsUrlObject.params.push(`access_token=${customAccessToken || config.ACCESS_TOKEN || ''}`); | |
| const payload: Object = { | |
| event: this.type, | |
| created: new Date(timestamp).toISOString(), | |
| sdkIdentifier: 'mapbox-gl-js', | |
| sdkVersion, | |
| skuId: SKU_ID, | |
| userId: this.anonId | |
| }; | |
| const finalPayload = additionalPayload ? extend(payload, additionalPayload) : payload; | |
| const request: RequestParameters = { | |
| url: formatUrl(eventsUrlObject), | |
| headers: { | |
| 'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request | |
| }, | |
| body: JSON.stringify([finalPayload]) | |
| }; | |
| this.pendingRequest = postData(request, (error) => { | |
| this.pendingRequest = null; | |
| callback(error); | |
| this.saveEventData(); | |
| this.processRequests(customAccessToken); | |
| }); | |
| } | |
| queueRequest(event: number | {id: number, timestamp: number}, customAccessToken?: ?string) { | |
| this.queue.push(event); | |
| this.processRequests(customAccessToken); | |
| } | |
| } | |
| export class MapLoadEvent extends TelemetryEvent { | |
| +success: {[_: number]: boolean}; | |
| skuToken: string; | |
| constructor() { | |
| super('map.load'); | |
| this.success = {}; | |
| this.skuToken = ''; | |
| } | |
| postMapLoadEvent(tileUrls: Array<string>, mapId: number, skuToken: string, customAccessToken: string) { | |
| this.skuToken = skuToken; | |
| const accessTokenIsSet = !!(customAccessToken || config.ACCESS_TOKEN); | |
| const usesMapboxTiles = Array.isArray(tileUrls) && tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url)); | |
| // Enabled only when Mapbox Access Token is set and a source uses mapbox tiles. | |
| if (config.EVENTS_URL && accessTokenIsSet && usesMapboxTiles) { | |
| this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken); | |
| } | |
| } | |
| processRequests(customAccessToken?: ?string) { | |
| if (this.pendingRequest || this.queue.length === 0) return; | |
| const {id, timestamp} = this.queue.shift(); | |
| // Only one load event should fire per map | |
| if (id && this.success[id]) return; | |
| if (!this.anonId) { | |
| this.fetchEventData(); | |
| } | |
| if (!validateUuid(this.anonId)) { | |
| this.anonId = uuid(); | |
| } | |
| this.postEvent(timestamp, {skuToken: this.skuToken}, (err) => { | |
| if (!err) { | |
| if (id) this.success[id] = true; | |
| } | |
| }, customAccessToken); | |
| } | |
| } | |
| export class TurnstileEvent extends TelemetryEvent { | |
| constructor(customAccessToken?: ?string) { | |
| super('appUserTurnstile'); | |
| this._customAccessToken = customAccessToken; | |
| } | |
| postTurnstileEvent(tileUrls: Array<string>, customAccessToken?: ?string) { | |
| //Enabled only when Mapbox Access Token is set and a source uses | |
| // mapbox tiles. | |
| if (config.EVENTS_URL && | |
| config.ACCESS_TOKEN && | |
| Array.isArray(tileUrls) && | |
| tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { | |
| this.queueRequest(Date.now(), customAccessToken); | |
| } | |
| } | |
| processRequests(customAccessToken?: ?string) { | |
| if (this.pendingRequest || this.queue.length === 0) { | |
| return; | |
| } | |
| if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) { | |
| //Retrieve cached data | |
| this.fetchEventData(); | |
| } | |
| const tokenData = parseAccessToken(config.ACCESS_TOKEN); | |
| const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN; | |
| //Reset event data cache if the access token owner changed. | |
| let dueForEvent = tokenU !== this.eventData.tokenU; | |
| if (!validateUuid(this.anonId)) { | |
| this.anonId = uuid(); | |
| dueForEvent = true; | |
| } | |
| const nextUpdate = this.queue.shift(); | |
| // Record turnstile event once per calendar day. | |
| if (this.eventData.lastSuccess) { | |
| const lastUpdate = new Date(this.eventData.lastSuccess); | |
| const nextDate = new Date(nextUpdate); | |
| const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); | |
| dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); | |
| } else { | |
| dueForEvent = true; | |
| } | |
| if (!dueForEvent) { | |
| return this.processRequests(); | |
| } | |
| this.postEvent(nextUpdate, {"enabled.telemetry": false}, (err) => { | |
| if (!err) { | |
| this.eventData.lastSuccess = nextUpdate; | |
| this.eventData.tokenU = tokenU; | |
| } | |
| }, customAccessToken); | |
| } | |
| } | |
| const turnstileEvent_ = new TurnstileEvent(); | |
| export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_); | |
| const mapLoadEvent_ = new MapLoadEvent(); | |
| export const postMapLoadEvent = mapLoadEvent_.postMapLoadEvent.bind(mapLoadEvent_); | |
| /***** END WARNING - REMOVAL OR MODIFICATION OF THE | |
| PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/ | |