summaryrefslogtreecommitdiffstats
path: root/browser/extensions/webcompat/shims/facebook-sdk.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/webcompat/shims/facebook-sdk.js')
-rw-r--r--browser/extensions/webcompat/shims/facebook-sdk.js554
1 files changed, 554 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/shims/facebook-sdk.js b/browser/extensions/webcompat/shims/facebook-sdk.js
new file mode 100644
index 0000000000..142f10ae33
--- /dev/null
+++ b/browser/extensions/webcompat/shims/facebook-sdk.js
@@ -0,0 +1,554 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Bug 1226498 - Shim Facebook SDK
+ *
+ * This shim provides functionality to enable Facebook's authenticator on third
+ * party sites ("continue/log in with Facebook" buttons). This includes rendering
+ * the button as the SDK would, if sites require it. This way, if users wish to
+ * opt into the Facebook login process regardless of the tracking consequences,
+ * they only need to click the button as usual.
+ *
+ * In addition, the shim also attempts to provide placeholders for Facebook
+ * videos, which users may click to opt into seeing the video (also despite
+ * the increased tracking risks). This is an experimental feature enabled
+ * that is only currently enabled on nightly builds.
+ *
+ * Finally, this shim also stubs out as much of the SDK as possible to prevent
+ * breaking on sites which expect that it will always successfully load.
+ */
+
+if (!window.FB) {
+ const FacebookLogoURL = "https://smartblock.firefox.etp/facebook.svg";
+ const PlayIconURL = "https://smartblock.firefox.etp/play.svg";
+
+ const originalUrl = document.currentScript.src;
+
+ let haveUnshimmed;
+ let initInfo;
+ let activeOnloginAttribute;
+ const placeholdersToRemoveOnUnshim = new Set();
+ const loggedGraphApiCalls = [];
+ const eventHandlers = new Map();
+
+ function getGUID() {
+ const v = crypto.getRandomValues(new Uint8Array(20));
+ return Array.from(v, c => c.toString(16)).join("");
+ }
+
+ const sendMessageToAddon = (function() {
+ const shimId = "FacebookSDK";
+ const pendingMessages = new Map();
+ const channel = new MessageChannel();
+ channel.port1.onerror = console.error;
+ channel.port1.onmessage = event => {
+ const { messageId, response } = event.data;
+ const resolve = pendingMessages.get(messageId);
+ if (resolve) {
+ pendingMessages.delete(messageId);
+ resolve(response);
+ }
+ };
+ function reconnect() {
+ const detail = {
+ pendingMessages: [...pendingMessages.values()],
+ port: channel.port2,
+ shimId,
+ };
+ window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
+ }
+ window.addEventListener("ShimHelperReady", reconnect);
+ reconnect();
+ return function(message) {
+ const messageId = getGUID();
+ return new Promise(resolve => {
+ const payload = { message, messageId, shimId };
+ pendingMessages.set(messageId, resolve);
+ channel.port1.postMessage(payload);
+ });
+ };
+ })();
+
+ const isNightly = sendMessageToAddon("getOptions").then(opts => {
+ return opts.releaseBranch === "nightly";
+ });
+
+ function makeLoginPlaceholder(target) {
+ // Sites may provide their own login buttons, or rely on the Facebook SDK
+ // to render one for them. For the latter case, we provide placeholders
+ // which try to match the examples and documentation here:
+ // https://developers.facebook.com/docs/facebook-login/web/login-button/
+
+ if (target.textContent || target.hasAttribute("fb-xfbml-state")) {
+ return;
+ }
+ target.setAttribute("fb-xfbml-state", "");
+
+ const size = target.getAttribute("data-size") || "large";
+
+ let font, margin, minWidth, maxWidth, height, iconHeight;
+ if (size === "small") {
+ font = 11;
+ margin = 8;
+ minWidth = maxWidth = 200;
+ height = 20;
+ iconHeight = 12;
+ } else if (size === "medium") {
+ font = 13;
+ margin = 8;
+ minWidth = 200;
+ maxWidth = 320;
+ height = 28;
+ iconHeight = 16;
+ } else {
+ font = 16;
+ minWidth = 240;
+ maxWidth = 400;
+ margin = 12;
+ height = 40;
+ iconHeight = 24;
+ }
+
+ const wattr = target.getAttribute("data-width") || "";
+ const width =
+ wattr === "100%" ? wattr : `${parseFloat(wattr) || minWidth}px`;
+
+ const round = target.getAttribute("data-layout") === "rounded" ? 20 : 4;
+
+ const text =
+ target.getAttribute("data-button-type") === "continue_with"
+ ? "Continue with Facebook"
+ : "Log in with Facebook";
+
+ const button = document.createElement("div");
+ button.style = `
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-left: ${margin + iconHeight}px;
+ ${width};
+ min-width: ${minWidth}px;
+ max-width: ${maxWidth}px;
+ height: ${height}px;
+ border-radius: ${round}px;
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ color: #fff;
+ font-size: ${font}px;
+ font-weight: bold;
+ font-family: Helvetica, Arial, sans-serif;
+ letter-spacing: .25px;
+ background-color: #1877f2;
+ background-repeat: no-repeat;
+ background-position: ${margin}px 50%;
+ background-size: ${iconHeight}px ${iconHeight}px;
+ background-image: url(${FacebookLogoURL});
+ `;
+ button.textContent = text;
+ target.appendChild(button);
+ target.addEventListener("click", () => {
+ activeOnloginAttribute = target.getAttribute("onlogin");
+ });
+ }
+
+ async function makeVideoPlaceholder(target) {
+ // For videos, we provide a more generic placeholder of roughly the
+ // expected size with a play button, as well as a Facebook logo.
+ if (!(await isNightly) || target.hasAttribute("fb-xfbml-state")) {
+ return;
+ }
+ target.setAttribute("fb-xfbml-state", "");
+
+ let width = parseInt(target.getAttribute("data-width"));
+ let height = parseInt(target.getAttribute("data-height"));
+ if (height) {
+ height = `${width * 0.6}px`;
+ } else {
+ height = `100%; min-height:${width * 0.75}px`;
+ }
+ if (width) {
+ width = `${width}px`;
+ } else {
+ width = `100%; min-width:200px`;
+ }
+
+ const placeholder = document.createElement("div");
+ placeholdersToRemoveOnUnshim.add(placeholder);
+ placeholder.style = `
+ width: ${width};
+ height: ${height};
+ top: 0px;
+ left: 0px;
+ background: #000;
+ color: #fff;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-image: url(${FacebookLogoURL}), url(${PlayIconURL});
+ background-position: calc(100% - 24px) 24px, 50% 47.5%;
+ background-repeat: no-repeat, no-repeat;
+ background-size: 43px 42px, 25% 25%;
+ -moz-text-size-adjust: none;
+ -moz-user-select: none;
+ color: #fff;
+ align-items: center;
+ padding-top: 200px;
+ font-size: 14pt;
+ `;
+ placeholder.textContent = "Click to allow blocked Facebook content";
+ placeholder.addEventListener("click", evt => {
+ if (!evt.isTrusted) {
+ return;
+ }
+ allowFacebookSDK(() => {
+ placeholdersToRemoveOnUnshim.forEach(p => p.remove());
+ });
+ });
+
+ target.innerHTML = "";
+ target.appendChild(placeholder);
+ }
+
+ // We monitor for XFBML objects as Facebook SDK does, so we
+ // can provide placeholders for dynamically-added ones.
+ const xfbmlObserver = new MutationObserver(mutations => {
+ for (let { addedNodes, target, type } of mutations) {
+ const nodes = type === "attributes" ? [target] : addedNodes;
+ for (const node of nodes) {
+ if (node?.classList?.contains("fb-login-button")) {
+ makeLoginPlaceholder(node);
+ }
+ if (node?.classList?.contains("fb-video")) {
+ makeVideoPlaceholder(node);
+ }
+ }
+ }
+ });
+
+ xfbmlObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["class"],
+ });
+
+ const needPopup =
+ !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name);
+ const popupName = getGUID();
+ let activePopup;
+
+ if (needPopup) {
+ const oldWindowOpen = window.open;
+ window.open = function(href, name, params) {
+ try {
+ const url = new URL(href, window.location.href);
+ if (
+ url.protocol === "https:" &&
+ (url.hostname === "m.facebook.com" ||
+ url.hostname === "www.facebook.com") &&
+ url.pathname.endsWith("/oauth")
+ ) {
+ name = popupName;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return oldWindowOpen.call(window, href, name, params);
+ };
+ }
+
+ let allowingFacebookPromise;
+
+ async function allowFacebookSDK(postInitCallback) {
+ if (allowingFacebookPromise) {
+ return allowingFacebookPromise;
+ }
+
+ let resolve, reject;
+ allowingFacebookPromise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
+
+ await sendMessageToAddon("optIn");
+
+ xfbmlObserver.disconnect();
+
+ const shim = window.FB;
+ window.FB = undefined;
+
+ // We need to pass the site's initialization info to the real
+ // SDK as it loads, so we use the fbAsyncInit mechanism to
+ // do so, also ensuring our own post-init callbacks are called.
+ const oldInit = window.fbAsyncInit;
+ window.fbAsyncInit = () => {
+ try {
+ if (typeof initInfo !== "undefined") {
+ window.FB.init(initInfo);
+ } else if (oldInit) {
+ oldInit();
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ // Also re-subscribe any SDK event listeners as early as possible.
+ for (const [name, fns] of eventHandlers.entries()) {
+ for (const fn of fns) {
+ window.FB.Event.subscribe(name, fn);
+ }
+ }
+
+ // Allow the shim to do any post-init work early as well, while the
+ // SDK script finishes loading and we ask it to re-parse XFBML etc.
+ postInitCallback?.();
+ };
+
+ const script = document.createElement("script");
+ script.src = originalUrl;
+
+ script.addEventListener("error", () => {
+ allowingFacebookPromise = null;
+ script.remove();
+ activePopup?.close();
+ window.FB = shim;
+ reject();
+ alert("Failed to load Facebook SDK; please try again");
+ });
+
+ script.addEventListener("load", () => {
+ haveUnshimmed = true;
+
+ // After the real SDK has fully loaded we re-issue any Graph API
+ // calls the page is waiting on, as well as requesting for it to
+ // re-parse any XBFML elements (including ones with placeholders).
+
+ for (const args of loggedGraphApiCalls) {
+ try {
+ window.FB.api.apply(window.FB, args);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ window.FB.XFBML.parse(document.body, resolve);
+ });
+
+ document.head.appendChild(script);
+
+ return allowingFacebookPromise;
+ }
+
+ function buildPopupParams() {
+ // We try to match Facebook's popup size reasonably closely.
+ const { outerWidth, outerHeight, screenX, screenY } = window;
+ const { width, height } = window.screen;
+ const w = Math.min(width, 400);
+ const h = Math.min(height, 400);
+ const ua = navigator.userAgent;
+ const isMobile = ua.includes("Mobile") || ua.includes("Tablet");
+ const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2;
+ const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5;
+ let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`;
+ if (!isMobile) {
+ params = `${params},width=${w},height=${h}`;
+ }
+ return params;
+ }
+
+ // If a page stores the window.FB reference of the shim, then we
+ // want to have it proxy calls to the real SDK once we've unshimmed.
+ function ensureProxiedToUnshimmed(obj) {
+ const shim = {};
+ for (const key in obj) {
+ const value = obj[key];
+ if (typeof value === "function") {
+ shim[key] = function() {
+ if (haveUnshimmed) {
+ return window.FB[key].apply(window.FB, arguments);
+ }
+ return value.apply(this, arguments);
+ };
+ } else if (typeof value !== "object" || value === null) {
+ shim[key] = value;
+ } else {
+ shim[key] = ensureProxiedToUnshimmed(value);
+ }
+ }
+ return new Proxy(shim, {
+ get: (shimmed, key) => (haveUnshimmed ? window.FB : shimmed)[key],
+ });
+ }
+
+ window.FB = ensureProxiedToUnshimmed({
+ api() {
+ loggedGraphApiCalls.push(arguments);
+ },
+ AppEvents: {
+ activateApp() {},
+ clearAppVersion() {},
+ clearUserID() {},
+ EventNames: {
+ ACHIEVED_LEVEL: "fb_mobile_level_achieved",
+ ADDED_PAYMENT_INFO: "fb_mobile_add_payment_info",
+ ADDED_TO_CART: "fb_mobile_add_to_cart",
+ ADDED_TO_WISHLIST: "fb_mobile_add_to_wishlist",
+ COMPLETED_REGISTRATION: "fb_mobile_complete_registration",
+ COMPLETED_TUTORIAL: "fb_mobile_tutorial_completion",
+ INITIATED_CHECKOUT: "fb_mobile_initiated_checkout",
+ PAGE_VIEW: "fb_page_view",
+ RATED: "fb_mobile_rate",
+ SEARCHED: "fb_mobile_search",
+ SPENT_CREDITS: "fb_mobile_spent_credits",
+ UNLOCKED_ACHIEVEMENT: "fb_mobile_achievement_unlocked",
+ VIEWED_CONTENT: "fb_mobile_content_view",
+ },
+ getAppVersion: () => "",
+ getUserID: () => "",
+ logEvent() {},
+ logPageView() {},
+ logPurchase() {},
+ ParameterNames: {
+ APP_USER_ID: "_app_user_id",
+ APP_VERSION: "_appVersion",
+ CONTENT_ID: "fb_content_id",
+ CONTENT_TYPE: "fb_content_type",
+ CURRENCY: "fb_currency",
+ DESCRIPTION: "fb_description",
+ LEVEL: "fb_level",
+ MAX_RATING_VALUE: "fb_max_rating_value",
+ NUM_ITEMS: "fb_num_items",
+ PAYMENT_INFO_AVAILABLE: "fb_payment_info_available",
+ REGISTRATION_METHOD: "fb_registration_method",
+ SEARCH_STRING: "fb_search_string",
+ SUCCESS: "fb_success",
+ },
+ setAppVersion() {},
+ setUserID() {},
+ updateUserProperties() {},
+ },
+ Canvas: {
+ getHash: () => "",
+ getPageInfo(cb) {
+ cb?.call(this, {
+ clientHeight: 1,
+ clientWidth: 1,
+ offsetLeft: 0,
+ offsetTop: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ });
+ },
+ Plugin: {
+ hidePluginElement() {},
+ showPluginElement() {},
+ },
+ Prefetcher: {
+ COLLECT_AUTOMATIC: 0,
+ COLLECT_MANUAL: 1,
+ addStaticResource() {},
+ setCollectionMode() {},
+ },
+ scrollTo() {},
+ setAutoGrow() {},
+ setDoneLoading() {},
+ setHash() {},
+ setSize() {},
+ setUrlHandler() {},
+ startTimer() {},
+ stopTimer() {},
+ },
+ Event: {
+ subscribe(e, f) {
+ if (!eventHandlers.has(e)) {
+ eventHandlers.set(e, new Set());
+ }
+ eventHandlers.get(e).add(f);
+ },
+ unsubscribe(e, f) {
+ eventHandlers.get(e)?.delete(f);
+ },
+ },
+ frictionless: {
+ init() {},
+ isAllowed: () => false,
+ },
+ gamingservices: {
+ friendFinder() {},
+ uploadImageToMediaLibrary() {},
+ },
+ getAccessToken: () => null,
+ getAuthResponse() {
+ return { status: "" };
+ },
+ getLoginStatus(cb) {
+ cb?.call(this, { status: "unknown" });
+ },
+ getUserID() {},
+ init(_initInfo) {
+ initInfo = _initInfo; // in case the site is not using fbAsyncInit
+ },
+ login(cb, opts) {
+ // We have to load Facebook's script, and then wait for it to call
+ // window.open. By that time, the popup blocker will likely trigger.
+ // So we open a popup now with about:blank, and then make sure FB
+ // will re-use that same popup later.
+ if (needPopup) {
+ activePopup = window.open("about:blank", popupName, buildPopupParams());
+ }
+ allowFacebookSDK(() => {
+ activePopup = undefined;
+ function runPostLoginCallbacks() {
+ try {
+ cb?.apply(this, arguments);
+ } catch (e) {
+ console.error(e);
+ }
+ if (activeOnloginAttribute) {
+ setTimeout(activeOnloginAttribute, 1);
+ activeOnloginAttribute = undefined;
+ }
+ }
+ window.FB.login(runPostLoginCallbacks, opts);
+ }).catch(() => {
+ activePopup = undefined;
+ activeOnloginAttribute = undefined;
+ try {
+ cb?.({});
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ },
+ logout(cb) {
+ cb?.call(this);
+ },
+ ui(params, fn) {
+ if (params.method === "permissions.oauth") {
+ window.FB.login(fn, params);
+ }
+ },
+ XFBML: {
+ parse(node, cb) {
+ node = node || document;
+ node.querySelectorAll(".fb-login-button").forEach(makeLoginPlaceholder);
+ node.querySelectorAll(".fb-video").forEach(makeVideoPlaceholder);
+ try {
+ cb?.call(this);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ },
+ });
+
+ window.FB.XFBML.parse();
+
+ window?.fbAsyncInit?.();
+}