From c0ea2aa41159f4c9a24a15f7e4a00337f020ee79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20K=C3=A4fer?= Date: Fri, 5 Oct 2018 17:59:45 +0200 Subject: [PATCH] use Fetch API if available and set correct referrer --- src/source/worker.js | 7 ++++- src/style/style.js | 4 ++- src/types/window.js | 3 ++ src/util/ajax.js | 66 ++++++++++++++++++++++++++++++++++++++------ test/ajax_stubs.js | 2 ++ 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/source/worker.js b/src/source/worker.js index 98fcb3a7419..85f1f15691d 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -32,6 +32,7 @@ export default class Worker { workerSourceTypes: { [string]: Class }; workerSources: { [string]: { [string]: { [string]: WorkerSource } } }; demWorkerSources: { [string]: { [string]: RasterDEMTileWorkerSource } }; + referrer: ?string; constructor(self: WorkerGlobalScopeInterface) { this.self = self; @@ -65,6 +66,10 @@ export default class Worker { }; } + setReferrer(mapID: string, referrer: string) { + this.referrer = referrer; + } + setLayers(mapId: string, layers: Array, callback: WorkerTileCallback) { this.getLayerIndex(mapId).replace(layers); callback(); @@ -196,5 +201,5 @@ export default class Worker { if (typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope) { - new Worker(self); + self.worker = new Worker(self); } diff --git a/src/style/style.js b/src/style/style.js index 2e9127dae4c..977fa39361f 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -11,7 +11,7 @@ import GlyphManager from '../render/glyph_manager'; import Light from './light'; import LineAtlas from '../render/line_atlas'; import { pick, clone, extend, deepEqual, filterObject, mapObject } from '../util/util'; -import { getJSON, ResourceType } from '../util/ajax'; +import { getJSON, getReferrer, ResourceType } from '../util/ajax'; import { isMapboxURL, normalizeStyleURL } from '../util/mapbox'; import browser from '../util/browser'; import Dispatcher from '../util/dispatcher'; @@ -144,6 +144,8 @@ class Style extends Evented { this._resetUpdates(); + this.dispatcher.broadcast('setReferrer', getReferrer()); + const self = this; this._rtlTextPluginCallback = Style.registerForPluginAvailability((args) => { self.dispatcher.broadcast('loadRTLTextPlugin', args.pluginURL, args.completionCallback); diff --git a/src/types/window.js b/src/types/window.js index 729bc21b3d2..54bf7d2f8c5 100644 --- a/src/types/window.js +++ b/src/types/window.js @@ -18,6 +18,7 @@ export interface Window extends EventTarget, IDBEnvironment { +isSecureContext: boolean; +length: number; +location: Location; + +origin: string; name: string; +navigator: Navigator; offscreenBuffering: string | boolean; @@ -131,6 +132,8 @@ export interface Window extends EventTarget, IDBEnvironment { WheelEvent: typeof WheelEvent; Worker: typeof Worker; XMLHttpRequest: typeof XMLHttpRequest; + Request: typeof Request; + AbortController: any; alert(message?: any): void; blur(): void; diff --git a/src/util/ajax.js b/src/util/ajax.js index f77dcf95758..d8fa3890392 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -2,6 +2,7 @@ import window from './window'; import { extend } from './util'; +import { isMapboxHTTPURL } from './mapbox'; import type { Callback } from '../types/callback'; import type { Cancelable } from '../types/cancelable'; @@ -40,7 +41,7 @@ export type RequestParameters = { headers?: Object, method?: 'GET' | 'POST' | 'PUT', body?: string, - type?: 'string' | 'json' | 'arraybuffer', + type?: 'string' | 'json' | 'arrayBuffer', credentials?: 'same-origin' | 'include', collectResourceTiming?: boolean }; @@ -51,6 +52,9 @@ class AJAXError extends Error { status: number; url: string; constructor(message: string, status: number, url: string) { + if (status === 401 && isMapboxHTTPURL(url)) { + message += ': you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens'; + } super(message); this.status = status; this.url = url; @@ -65,11 +69,57 @@ class AJAXError extends Error { } } -function makeRequest(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { +// Ensure that we're sending the correct referrer from blob URL worker bundles. +// For files loaded from the local file system, `location.origin` will be set +// to the string(!) "null" (Firefox), or "file://" (Chrome, Safari, Edge, IE), +// and we will set an empty referrer. Otherwise, we're using the document's URL. +/* global self, WorkerGlobalScope */ +export const getReferrer = typeof WorkerGlobalScope !== 'undefined' && + typeof self !== 'undefined' && + self instanceof WorkerGlobalScope ? + () => self.worker && self.worker.referrer : + () => { + const origin = window.location.origin; + if (origin && origin !== 'null' && origin !== 'file://') { + return origin + window.location.pathname; + } + }; + +function makeFetchRequest(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { + const controller = new window.AbortController(); + const request = new window.Request(requestParameters.url, { + method: requestParameters.method || 'GET', + body: requestParameters.body, + credentials: requestParameters.credentials, + headers: requestParameters.headers, + referrer: getReferrer(), + signal: controller.signal + }); + + if (requestParameters.type === 'json') { + request.headers.set('Accept', 'application/json'); + } + + window.fetch(request).then(response => { + if (response.ok) { + response[requestParameters.type || 'text']().then(result => { + callback(null, result, response.headers.get('Cache-Control'), response.headers.get('Expires')); + }).catch(callback); + } else { + callback(new AJAXError(response.statusText, response.status, requestParameters.url)); + } + }).catch((error) => { + callback(new Error(error.message)); + }); + + return { cancel: () => controller.abort() }; +} + +function makeXMLHttpRequest(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { const xhr: XMLHttpRequest = new window.XMLHttpRequest(); xhr.open(requestParameters.method || 'GET', requestParameters.url, true); - if (requestParameters.type === 'arraybuffer') { + if (requestParameters.type === 'arrayBuffer') { xhr.responseType = 'arraybuffer'; } for (const k in requestParameters.headers) { @@ -95,23 +145,21 @@ function makeRequest(requestParameters: RequestParameters, callback: ResponseCal } callback(null, data, xhr.getResponseHeader('Cache-Control'), xhr.getResponseHeader('Expires')); } else { - if (xhr.status === 401 && requestParameters.url.match(/mapbox.com/)) { - callback(new AJAXError(`${xhr.statusText}: you may have provided an invalid Mapbox access token. See https://www.mapbox.com/api-documentation/#access-tokens`, xhr.status, requestParameters.url)); - } else { - callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); - } + callback(new AJAXError(xhr.statusText, xhr.status, requestParameters.url)); } }; xhr.send(requestParameters.body); return { cancel: () => xhr.abort() }; } +const makeRequest = window.fetch && window.Request && window.AbortController ? makeFetchRequest : makeXMLHttpRequest; + export const getJSON = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { return makeRequest(extend(requestParameters, { type: 'json' }), callback); }; export const getArrayBuffer = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { - return makeRequest(extend(requestParameters, { type: 'arraybuffer' }), callback); + return makeRequest(extend(requestParameters, { type: 'arrayBuffer' }), callback); }; export const postData = function(requestParameters: RequestParameters, callback: ResponseCallback): Cancelable { diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index 8ecceaf3d80..d3e808ff8c2 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -34,6 +34,8 @@ function cached(data, callback) { }); } +export const getReferrer = () => undefined; + export const getJSON = function({ url }, callback) { if (cache[url]) return cached(cache[url], callback); return request(url, (error, response, body) => {