diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 5c5dccea7c3..46e7dc729b6 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -5,7 +5,7 @@ import config from './config'; import browser from './browser'; import window from './window'; import { version } from '../../package.json'; -import { uuid, validateUuid, storageAvailable, warnOnce, extend } from './util'; +import { uuid, validateUuid, storageAvailable, b64DecodeUnicode, b64EncodeUnicode, warnOnce, extend } from './util'; import { postData } from './ajax'; import type { RequestParameters } from './ajax'; @@ -158,10 +158,28 @@ function formatUrl(obj: UrlObject): string { return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } +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: { lastSuccess: ?number, accessToken: ?string}; + eventData: any; anonId: ?string; queue: Array; type: TelemetryEventType; @@ -170,15 +188,28 @@ class TelemetryEvent { constructor(type: TelemetryEventType) { this.type = type; this.anonId = null; - this.eventData = {lastSuccess: null, accessToken: config.ACCESS_TOKEN}; + 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 = `${telemEventKey}:${config.ACCESS_TOKEN || ''}`; - const uuidKey = `${telemEventKey}.uuid:${config.ACCESS_TOKEN || ''}`; + const storageKey = this.getStorageKey(); + const uuidKey = this.getStorageKey('uuid'); if (isLocalStorageAvailable) { //Retrieve cached data @@ -198,12 +229,12 @@ class TelemetryEvent { saveEventData() { const isLocalStorageAvailable = storageAvailable('localStorage'); - const storageKey = `${telemEventKey}:${config.ACCESS_TOKEN || ''}`; - const uuidKey = `${telemEventKey}.uuid:${config.ACCESS_TOKEN || ''}`; + const storageKey = this.getStorageKey(); + const uuidKey = this.getStorageKey('uuid'); if (isLocalStorageAvailable) { try { window.localStorage.setItem(uuidKey, this.anonId); - if (this.eventData.lastSuccess) { + if (Object.keys(this.eventData).length >= 1) { window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); } } catch (e) { @@ -267,7 +298,7 @@ export class MapLoadEvent extends TelemetryEvent { // mapbox tiles. if (config.ACCESS_TOKEN && Array.isArray(tileUrls) && - tileUrls.some(url => isMapboxHTTPURL(url))) { + tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { this.queueRequest({id: mapId, timestamp: Date.now()}); } } @@ -306,7 +337,7 @@ export class TurnstileEvent extends TelemetryEvent { // mapbox tiles. if (config.ACCESS_TOKEN && Array.isArray(tileUrls) && - tileUrls.some(url => isMapboxHTTPURL(url))) { + tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) { this.queueRequest(Date.now()); } } @@ -317,16 +348,16 @@ export class TurnstileEvent extends TelemetryEvent { return; } - let dueForEvent = this.eventData.accessToken ? (this.eventData.accessToken !== config.ACCESS_TOKEN) : false; - //Reset event data cache if the access token changed. - if (dueForEvent) { - this.anonId = this.eventData.lastSuccess = null; - } - if (!this.anonId || !this.eventData.lastSuccess) { + 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; @@ -350,7 +381,7 @@ export class TurnstileEvent extends TelemetryEvent { this.postEvent(nextUpdate, {"enabled.telemetry": false}, (err) => { if (!err) { this.eventData.lastSuccess = nextUpdate; - this.eventData.accessToken = config.ACCESS_TOKEN; + this.eventData.tokenU = tokenU; } }); } diff --git a/src/util/util.js b/src/util/util.js index b4274a30eed..e39b4e2ca4e 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -447,3 +447,23 @@ export function storageAvailable(type: string): boolean { return false; } } + +// The following methods are from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem +//Unicode compliant base64 encoder for strings +export function b64EncodeUnicode(str: string) { + return window.btoa( + encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, + (match, p1) => { + return String.fromCharCode(Number('0x' + p1)); //eslint-disable-line + } + ) + ); +} + + +// Unicode compliant decoder for base64-encoded strings +export function b64DecodeUnicode(str: string) { + return decodeURIComponent(window.atob(str).split('').map((c) => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); //eslint-disable-line + }).join('')); +} diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index c3b6fc50f88..7fa7c977e16 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -453,7 +453,8 @@ test("mapbox", (t) => { const now = +Date.now(); window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, uuid()); window.localStorage.setItem(`mapbox.eventData:${config.ACCESS_TOKEN}`, JSON.stringify({ - lastSuccess: now + lastSuccess: now, + tokenU: 'key' })); // Post 5 seconds later