media/js/base/mozilla-client.js (317 lines of code) (raw):
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Create namespace
if (typeof window.Mozilla === 'undefined') {
window.Mozilla = {};
}
(function () {
'use strict';
/**
* Provide information on the user's browsing environment, including the platform and browser details.
*
* @namespace
* @see {@link https://developer.mozilla.org/en-US/docs/Gecko_user_agent_string_reference}
*/
var Client = {};
/**
* Minimum Firefox version supported by FxA.
* https://mozilla.github.io/ecosystem-platform/docs/fxa-engineering/fxa-dev-process#browser-support
*/
Client.FxALastSupported = 60;
/**
* Detect whether the user's browser is Firefox on any platform. This includes WebKit-based Firefox for iOS.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Boolean} result
*/
Client._isFirefox = function (ua) {
ua = ua || navigator.userAgent;
return /\s(Firefox|FxiOS)/.test(ua) && !Client._isLikeFirefox(ua);
};
/**
* Detect whether the user's browser is Firefox for Windows, OS X or Linux.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Boolean} result
*/
Client._isFirefoxDesktop = function (ua) {
ua = ua || navigator.userAgent;
return (
/\sFirefox/.test(ua) &&
!/Mobile|Tablet|Fennec/.test(ua) &&
!Client._isLikeFirefox(ua)
);
};
/**
* Detect whether the user's browser is Firefox for Android.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Boolean} result
*/
Client._isFirefoxAndroid = function (ua) {
ua = ua || navigator.userAgent;
return /\sFirefox/.test(ua) && /Android/.test(ua);
};
/**
* Detect whether the user's browser is Firefox for iOS.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Boolean} result
*/
Client._isFirefoxiOS = function (ua) {
ua = ua || navigator.userAgent;
return /FxiOS/.test(ua);
};
/**
* Detect whether the user's browser is Gecko-based.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Boolean} result
*/
Client._isLikeFirefox = function (ua) {
ua = ua || navigator.userAgent;
return /Iceweasel|IceCat|SeaMonkey|Camino|like Firefox/i.test(ua);
};
/**
* Get the user's Firefox version number. '0' will be returned on Firefox for iOS and non-Firefox browsers.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {String} version number
*/
Client._getFirefoxVersion = function (ua) {
ua = ua || navigator.userAgent;
var matches = /Firefox\/(\d+(?:\.\d+){1,2})/.exec(ua);
return matches && !Client._isLikeFirefox(ua) ? matches[1] : '0';
};
/**
* Get the user's Firefox major version number. 0 will be returned on Firefox for iOS and non-Firefox browsers.
*
* @private
* @param {String} ua - browser's user agent string, navigator.userAgent is used if not specified
* @return {Number} major version number in integer
*/
Client._getFirefoxMajorVersion = function (ua) {
return parseInt(Client._getFirefoxVersion(ua), 10);
};
/**
* Determine if user version is up to date with latest version from product details.
*
* @private
* @param {Boolean} strict - if false compare the major version number only.
* @param {Array} userVerArr - the user version number.
* @param {Array} latestVerArr - the latest version number from product details.
* @return {Boolean} true if user version number is equal to or greater than product details version.
*/
Client._compareVersion = function (strict, userVerArr, latestVerArr) {
var currentUserNumber = 0;
var currentLatestNumber = 0;
var isUpToDate = false;
// Make sure both latest and user array lengths match.
while (latestVerArr.length < userVerArr.length) {
latestVerArr.push('0');
}
while (userVerArr.length < latestVerArr.length) {
userVerArr.push('0');
}
// Only check the major version in non-strict comparison mode.
if (!strict) {
latestVerArr.length = 1;
}
// Step through the array from product details and compare to the user array.
for (var j = 0; j < latestVerArr.length; j++) {
currentUserNumber = Number(userVerArr[j]);
currentLatestNumber = Number(latestVerArr[j]);
if (currentUserNumber < currentLatestNumber) {
isUpToDate = false;
break;
} else if (currentUserNumber > currentLatestNumber) {
isUpToDate = true;
break;
} else {
isUpToDate = true;
}
}
return isUpToDate;
};
/**
* Detect whether the user's Firefox is up to date or outdated. This data is mainly used for security notifications.
*
* @private
* @param {Boolean} strict - whether the minor and patch-level version numbers should be compared. Default: true
* @param {Boolean} isESR - whether the Firefox update channel is ESR. Default: false
* @param {String} userVer - browser's version number
* @return {Boolean} result
*/
Client._isFirefoxUpToDate = function (strict, isESR, userVer) {
strict = strict === undefined ? true : strict;
isESR = isESR === undefined ? false : isESR;
userVer = userVer === undefined ? Client._getFirefoxVersion() : userVer;
var html = document.documentElement;
if (
!html.getAttribute('data-esr-versions') ||
!html.getAttribute('data-latest-firefox')
) {
return false;
}
var versions = isESR
? html.getAttribute('data-esr-versions').split(' ')
: [html.getAttribute('data-latest-firefox')];
var userVerArr = userVer.match(/^(\d+(?:\.\d+){1,2})/);
if (userVerArr && userVerArr.length > 0) {
userVerArr = userVerArr[1].split('.');
} else {
// if there's no version in the UA string, assume out of date (issue 10582)
return false;
}
var isUpToDate = false;
// Sort product details version so we compare the newer version first
versions.sort(function (a, b) {
return parseFloat(a) < parseFloat(b);
});
// Compare each latest version in product details to the user version.
for (var i = 0; i < versions.length; i++) {
var latestVerArr = versions[i].split('.');
isUpToDate = Client._compareVersion(
strict,
userVerArr,
latestVerArr
);
if (isUpToDate) {
break;
}
}
return isUpToDate;
};
/**
* Use the async mozUITour API of Firefox to retrieve the user's browser info, including the update channel and
* accurate, patch-level version number. This API is available on Firefox 35 and later. See
* https://mozilla.github.io/bedrock/uitour/ for details.
*
* @param {Function} callback - callback function to be executed with the Firefox details
* @return {None}
*/
Client.getFirefoxDetails = function (callback) {
// Fire the callback function immediately if cache exists
if (Client.FirefoxDetails) {
callback(Client.FirefoxDetails);
return;
}
var callbackID = Math.random()
.toString(36)
.replace(/[^a-z]+/g, '');
var listener = function (event) {
if (
!event.detail ||
!event.detail.data ||
event.detail.callbackID !== callbackID
) {
return;
}
window.clearTimeout(timer);
onRetrieved(
true,
event.detail.data.version,
event.detail.data.defaultUpdateChannel,
event.detail.data.distribution
);
};
var onRetrieved = function (accurate, version, channel, distribution) {
document.removeEventListener('mozUITourResponse', listener, false);
var isESR = channel === 'esr';
var isUpToDate = Client._isFirefoxUpToDate(
accurate,
accurate ? isESR : false,
version
);
var details = (Client.FirefoxDetails = {
accurate: accurate,
version: version,
channel: channel,
distribution: distribution,
isUpToDate: isUpToDate,
isESR: isESR
});
callback(details);
};
// Prepare fallback function in case the API doesn't work
var userVer = Client._getFirefoxVersion();
var fallback = function () {
onRetrieved(false, userVer, 'release', undefined);
};
// If Firefox is old or for Android, call the fallback function immediately because the API is not implemented
if (parseFloat(userVer) < 35 || Client._isFirefoxAndroid()) {
fallback();
return;
}
// Fire the fallback function in .4 seconds
var timer = window.setTimeout(fallback, 400);
// Trigger the API
document.addEventListener('mozUITourResponse', listener, false);
document.dispatchEvent(
new CustomEvent('mozUITour', {
bubbles: true,
detail: {
action: 'getConfiguration',
data: { configuration: 'appinfo', callbackID: callbackID }
}
})
);
};
/**
* Use the async mozUITour API of Firefox to retrieve the user's FxA info. See
* https://mozilla.github.io/bedrock/uitour/ for details.
*
* The various states here are... complicated
* This is the intention:
* - firefox: true if Firefox
* - legacy: true if older than FxALastSupported
* - mobile: false | android | ios
* - setup: true if Fx >= 29 and logged into *Sync*.
* true if Fx >= 74 and logged into *FxA*.
* - browserServices.sync
* setup: logged into Sync.
* desktopDevices: number of desktop devices synced.
* mobileDevices: number of mobile devices synced.
* totalDevices: number of total devices synced.
* Notes:
* - Fx < 50 has FxA and UITour support but the API does not return device counts
* - Fx < FxALastSupported accounts.firefox.com does not work
* - FxALastSupported is supplied by the FxA team
* - these versions are still capable of logging in through the browser
* - differentiated because we generally do not give these versions the FxA calls to action (eg. "Create an Account")
* - Fx < 29 the mozUITour API is not available, though the user may still be logged in
* - If you're curious, "sync" began with Fx 4.
*
* @param {Function} callback - callback function to be executed with the FxA details
* @return {None}
*/
Client.getFxaDetails = function (callback) {
// Fire the callback function immediately if FxaDetails are already defined
if (Client.FxaDetails) {
callback(Client.FxaDetails);
return;
}
var request = {
name: null,
callback: null
};
// Set up the object with default values of false
var details = {
firefox: false,
legacy: false,
mobile: false,
setup: false,
browserServices: {
sync: {
setup: false,
desktopDevices: false,
mobileDevices: false,
totalDevices: false
}
}
};
// Override object values as we get more information
if (Client._isFirefoxAndroid()) {
details.firefox = true;
details.mobile = 'android';
returnFxaDetails();
return;
} else if (Client._isFirefoxiOS()) {
details.firefox = true;
details.mobile = 'ios';
returnFxaDetails();
return;
} else if (Client._isFirefoxDesktop()) {
details.firefox = true;
var userVer = parseFloat(Client._getFirefoxVersion());
if (userVer < 29) {
// UITour not supported
details.legacy = true;
returnFxaDetails();
return;
} else {
// UITour supported
// still note if it's older than accounts.firefox.com supports
if (userVer < Client.FxALastSupported) {
details.legacy = true;
}
// callbackID to make sure we're responding to our request
var callbackID = Math.random()
.toString(36)
.replace(/[^a-z]+/g, '');
// UITour API response event handler for 'sync', checks for callbackID
var listenerSync = function (event) {
if (
!event.detail ||
!event.detail.data ||
event.detail.callbackID !== callbackID
) {
return;
}
var config = event.detail.data;
// Clear the timeout and remove the event listener.
window.clearTimeout(timer);
document.removeEventListener(
'mozUITourResponse',
listenerSync,
false
);
/**
* Account signed-in state
* Assume being signed-in to Sync equals being signed in to an account.
*/
details.setup = config.setup;
/**
* Browser services
* Device counts are only available in Fx50+, fallback 'unknown' if not detectable
*/
details.browserServices.sync = {
setup: config.setup,
desktopDevices: Object.prototype.hasOwnProperty.call(
config,
'desktopDevices'
)
? config.desktopDevices
: 'unknown',
mobileDevices: Object.prototype.hasOwnProperty.call(
config,
'mobileDevices'
)
? config.mobileDevices
: 'unknown',
totalDevices: Object.prototype.hasOwnProperty.call(
config,
'totalDevices'
)
? config.totalDevices
: 'unknown'
};
returnFxaDetails();
};
// UITour API response event handler for 'fxa', checks for callbackID
var listenerFxA = function (event) {
if (
!event.detail ||
!event.detail.data ||
event.detail.callbackID !== callbackID
) {
return;
}
var config = event.detail.data;
// Clear the timeout and remove the event listener.
window.clearTimeout(timer);
document.removeEventListener(
'mozUITourResponse',
listenerFxA,
false
);
// Account signed-in state
details.setup = config.setup;
// Browser services (Sync is the only service here currently).
details.browserServices = config.browserServices;
returnFxaDetails();
};
// Query Mozilla account signed In stage via 'fxa' config.
if (userVer >= 74) {
request.name = 'fxa';
request.callback = listenerFxA;
}
// UITour supported but client must use legacy 'sync' configuration call.
else {
request.name = 'sync';
request.callback = listenerSync;
}
// Trigger the UITour API and start listening for the reponse
document.addEventListener(
'mozUITourResponse',
request.callback,
false
);
document.dispatchEvent(
new CustomEvent('mozUITour', {
bubbles: true,
detail: {
action: 'getConfiguration',
data: {
configuration: request.name,
callbackID: callbackID
}
}
})
);
}
}
function returnFxaDetails() {
window.clearTimeout(timer);
Client.FxaDetails = details;
callback(details);
}
// Fire the fallback function in .4 seconds
var timer = window.setTimeout(returnFxaDetails, 400);
};
// Append static properties for faster access
Client.isFirefox = Client._isFirefox();
Client.isFirefoxDesktop = Client._isFirefoxDesktop();
Client.isFirefoxAndroid = Client._isFirefoxAndroid();
Client.isFirefoxiOS = Client._isFirefoxiOS();
Client.isLikeFirefox = Client._isLikeFirefox();
Client.FirefoxVersion = Client._getFirefoxVersion();
Client.FirefoxMajorVersion = Client._getFirefoxMajorVersion();
// Append platform info as well for convenience
Client.platform = window.site.platform;
Client.isMobile = /^(android|ios)$/.test(Client.platform);
Client.isDesktop = !Client.isMobile;
window.Mozilla.Client = Client;
})();