forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			554 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			554 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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?.();
 | |
| }
 | 
