facebook-sdk.js (17519B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * Bug 1226498 - Shim Facebook SDK 9 * 10 * This shim provides functionality to enable Facebook's authenticator on third 11 * party sites ("continue/log in with Facebook" buttons). This includes rendering 12 * the button as the SDK would, if sites require it. This way, if users wish to 13 * opt into the Facebook login process regardless of the tracking consequences, 14 * they only need to click the button as usual. 15 * 16 * In addition, the shim also attempts to provide placeholders for Facebook 17 * videos, which users may click to opt into seeing the video (also despite 18 * the increased tracking risks). This is an experimental feature enabled 19 * that is only currently enabled on nightly builds. 20 * 21 * Finally, this shim also stubs out as much of the SDK as possible to prevent 22 * breaking on sites which expect that it will always successfully load. 23 */ 24 25 if (!window.FB) { 26 const FacebookLogoURL = "https://smartblock.firefox.etp/facebook.svg"; 27 const PlayIconURL = "https://smartblock.firefox.etp/play.svg"; 28 29 const originalUrl = (() => { 30 const src = document.currentScript?.src; 31 try { 32 const { protocol, hostname, pathname, href } = new URL(src); 33 if ( 34 (protocol === "http:" || protocol === "https:") && 35 hostname === "connect.facebook.net" && 36 (pathname.endsWith("/sdk.js") || pathname.endsWith("/all.js")) 37 ) { 38 return href; 39 } 40 if (href.includes("all.js")) { 41 // Legacy SDK. 42 return "https://connect.facebook.net/en_US/all.js"; 43 } 44 } catch (_) {} 45 return "https://connect.facebook.net/en_US/sdk.js"; 46 })(); 47 48 let haveUnshimmed; 49 let initInfo; 50 let activeOnloginAttribute; 51 const placeholdersToRemoveOnUnshim = new Set(); 52 const loggedGraphApiCalls = []; 53 const eventHandlers = new Map(); 54 55 function getGUID() { 56 const v = crypto.getRandomValues(new Uint8Array(20)); 57 return Array.from(v, c => c.toString(16)).join(""); 58 } 59 60 const sendMessageToAddon = (function () { 61 const shimId = "FacebookSDK"; 62 const pendingMessages = new Map(); 63 const channel = new MessageChannel(); 64 channel.port1.onerror = console.error; 65 channel.port1.onmessage = event => { 66 const { messageId, response } = event.data; 67 const resolve = pendingMessages.get(messageId); 68 if (resolve) { 69 pendingMessages.delete(messageId); 70 resolve(response); 71 } 72 }; 73 function reconnect() { 74 const detail = { 75 pendingMessages: [...pendingMessages.values()], 76 port: channel.port2, 77 shimId, 78 }; 79 window.dispatchEvent(new CustomEvent("ShimConnects", { detail })); 80 } 81 window.addEventListener("ShimHelperReady", reconnect); 82 reconnect(); 83 return function (message) { 84 const messageId = getGUID(); 85 return new Promise(resolve => { 86 const payload = { message, messageId, shimId }; 87 pendingMessages.set(messageId, resolve); 88 channel.port1.postMessage(payload); 89 }); 90 }; 91 })(); 92 93 const isNightly = sendMessageToAddon("getOptions").then(opts => { 94 return opts.releaseBranch === "nightly"; 95 }); 96 97 function makeLoginPlaceholder(target) { 98 // Sites may provide their own login buttons, or rely on the Facebook SDK 99 // to render one for them. For the latter case, we provide placeholders 100 // which try to match the examples and documentation here: 101 // https://developers.facebook.com/docs/facebook-login/web/login-button/ 102 103 if (target.textContent || target.hasAttribute("fb-xfbml-state")) { 104 return; 105 } 106 target.setAttribute("fb-xfbml-state", ""); 107 108 const size = target.getAttribute("data-size") || "large"; 109 110 let font, margin, minWidth, maxWidth, height, iconHeight; 111 if (size === "small") { 112 font = 11; 113 margin = 8; 114 minWidth = maxWidth = 200; 115 height = 20; 116 iconHeight = 12; 117 } else if (size === "medium") { 118 font = 13; 119 margin = 8; 120 minWidth = 200; 121 maxWidth = 320; 122 height = 28; 123 iconHeight = 16; 124 } else { 125 font = 16; 126 minWidth = 240; 127 maxWidth = 400; 128 margin = 12; 129 height = 40; 130 iconHeight = 24; 131 } 132 133 const wattr = target.getAttribute("data-width") || ""; 134 const width = 135 wattr === "100%" ? wattr : `${parseFloat(wattr) || minWidth}px`; 136 137 const round = target.getAttribute("data-layout") === "rounded" ? 20 : 4; 138 139 const text = 140 target.getAttribute("data-button-type") === "continue_with" 141 ? "Continue with Facebook" 142 : "Log in with Facebook"; 143 144 const button = document.createElement("div"); 145 button.style = ` 146 display: flex; 147 align-items: center; 148 justify-content: center; 149 padding-left: ${margin + iconHeight}px; 150 ${width}; 151 min-width: ${minWidth}px; 152 max-width: ${maxWidth}px; 153 height: ${height}px; 154 border-radius: ${round}px; 155 -moz-text-size-adjust: none; 156 -moz-user-select: none; 157 color: #fff; 158 font-size: ${font}px; 159 font-weight: bold; 160 font-family: Helvetica, Arial, sans-serif; 161 letter-spacing: .25px; 162 background-color: #1877f2; 163 background-repeat: no-repeat; 164 background-position: ${margin}px 50%; 165 background-size: ${iconHeight}px ${iconHeight}px; 166 background-image: url(${FacebookLogoURL}); 167 `; 168 button.textContent = text; 169 target.appendChild(button); 170 target.addEventListener("click", () => { 171 activeOnloginAttribute = target.getAttribute("onlogin"); 172 }); 173 } 174 175 async function makeVideoPlaceholder(target) { 176 // For videos, we provide a more generic placeholder of roughly the 177 // expected size with a play button, as well as a Facebook logo. 178 if (!(await isNightly) || target.hasAttribute("fb-xfbml-state")) { 179 return; 180 } 181 target.setAttribute("fb-xfbml-state", ""); 182 183 let width = parseInt(target.getAttribute("data-width")); 184 let height = parseInt(target.getAttribute("data-height")); 185 if (height) { 186 height = `${width * 0.6}px`; 187 } else { 188 height = `100%; min-height:${width * 0.75}px`; 189 } 190 if (width) { 191 width = `${width}px`; 192 } else { 193 width = `100%; min-width:200px`; 194 } 195 196 const placeholder = document.createElement("div"); 197 placeholdersToRemoveOnUnshim.add(placeholder); 198 placeholder.style = ` 199 width: ${width}; 200 height: ${height}; 201 top: 0px; 202 left: 0px; 203 background: #000; 204 color: #fff; 205 text-align: center; 206 cursor: pointer; 207 display: flex; 208 align-items: center; 209 justify-content: center; 210 background-image: url(${FacebookLogoURL}), url(${PlayIconURL}); 211 background-position: calc(100% - 24px) 24px, 50% 47.5%; 212 background-repeat: no-repeat, no-repeat; 213 background-size: 43px 42px, 25% 25%; 214 -moz-text-size-adjust: none; 215 -moz-user-select: none; 216 color: #fff; 217 align-items: center; 218 padding-top: 200px; 219 font-size: 14pt; 220 `; 221 placeholder.textContent = "Click to allow blocked Facebook content"; 222 placeholder.addEventListener("click", evt => { 223 if (!evt.isTrusted) { 224 return; 225 } 226 allowFacebookSDK(() => { 227 placeholdersToRemoveOnUnshim.forEach(p => p.remove()); 228 }); 229 }); 230 231 target.innerHTML = ""; 232 target.appendChild(placeholder); 233 } 234 235 // We monitor for XFBML objects as Facebook SDK does, so we 236 // can provide placeholders for dynamically-added ones. 237 const xfbmlObserver = new MutationObserver(mutations => { 238 for (let { addedNodes, target, type } of mutations) { 239 const nodes = type === "attributes" ? [target] : addedNodes; 240 for (const node of nodes) { 241 if (node?.classList?.contains("fb-login-button")) { 242 makeLoginPlaceholder(node); 243 } 244 if (node?.classList?.contains("fb-video")) { 245 makeVideoPlaceholder(node); 246 } 247 } 248 } 249 }); 250 251 xfbmlObserver.observe(document.documentElement, { 252 childList: true, 253 subtree: true, 254 attributes: true, 255 attributeFilter: ["class"], 256 }); 257 258 const needPopup = 259 !/app_runner/.test(window.name) && !/iframe_canvas/.test(window.name); 260 const popupName = getGUID(); 261 let activePopup; 262 263 if (needPopup) { 264 const oldWindowOpen = window.open; 265 window.open = function (href, name, params) { 266 try { 267 const url = new URL(href, window.location.href); 268 if ( 269 url.protocol === "https:" && 270 (url.hostname === "m.facebook.com" || 271 url.hostname === "www.facebook.com") && 272 url.pathname.endsWith("/oauth") 273 ) { 274 name = popupName; 275 } 276 } catch (e) { 277 console.error(e); 278 } 279 return oldWindowOpen.call(window, href, name, params); 280 }; 281 } 282 283 let allowingFacebookPromise; 284 285 async function allowFacebookSDK(postInitCallback) { 286 if (allowingFacebookPromise) { 287 return allowingFacebookPromise; 288 } 289 290 let resolve, reject; 291 allowingFacebookPromise = new Promise((_resolve, _reject) => { 292 resolve = _resolve; 293 reject = _reject; 294 }); 295 296 await sendMessageToAddon("optIn"); 297 298 xfbmlObserver.disconnect(); 299 300 const shim = window.FB; 301 window.FB = undefined; 302 303 // We need to pass the site's initialization info to the real 304 // SDK as it loads, so we use the fbAsyncInit mechanism to 305 // do so, also ensuring our own post-init callbacks are called. 306 const oldInit = window.fbAsyncInit; 307 window.fbAsyncInit = () => { 308 try { 309 if (typeof initInfo !== "undefined") { 310 window.FB.init(initInfo); 311 } else if (oldInit) { 312 oldInit(); 313 } 314 } catch (e) { 315 console.error(e); 316 } 317 318 // Also re-subscribe any SDK event listeners as early as possible. 319 for (const [name, fns] of eventHandlers.entries()) { 320 for (const fn of fns) { 321 window.FB.Event.subscribe(name, fn); 322 } 323 } 324 325 // Allow the shim to do any post-init work early as well, while the 326 // SDK script finishes loading and we ask it to re-parse XFBML etc. 327 postInitCallback?.(); 328 }; 329 330 const script = document.createElement("script"); 331 script.src = originalUrl; 332 333 script.addEventListener("error", () => { 334 allowingFacebookPromise = null; 335 script.remove(); 336 activePopup?.close(); 337 window.FB = shim; 338 reject(); 339 alert("Failed to load Facebook SDK; please try again"); 340 }); 341 342 script.addEventListener("load", () => { 343 haveUnshimmed = true; 344 345 // After the real SDK has fully loaded we re-issue any Graph API 346 // calls the page is waiting on, as well as requesting for it to 347 // re-parse any XBFML elements (including ones with placeholders). 348 349 for (const args of loggedGraphApiCalls) { 350 try { 351 window.FB.api.apply(window.FB, args); 352 } catch (e) { 353 console.error(e); 354 } 355 } 356 357 window.FB.XFBML.parse(document.body, resolve); 358 }); 359 360 document.head.appendChild(script); 361 362 return allowingFacebookPromise; 363 } 364 365 function buildPopupParams() { 366 // We try to match Facebook's popup size reasonably closely. 367 const { outerWidth, outerHeight, screenX, screenY } = window; 368 const { width, height } = window.screen; 369 const w = Math.min(width, 400); 370 const h = Math.min(height, 400); 371 const ua = navigator.userAgent; 372 const isMobile = ua.includes("Mobile") || ua.includes("Tablet"); 373 const left = screenX + (screenX < 0 ? width : 0) + (outerWidth - w) / 2; 374 const top = screenY + (screenY < 0 ? height : 0) + (outerHeight - h) / 2.5; 375 let params = `left=${left},top=${top},width=${w},height=${h},scrollbars=1,toolbar=0,location=1`; 376 if (!isMobile) { 377 params = `${params},width=${w},height=${h}`; 378 } 379 return params; 380 } 381 382 // If a page stores the window.FB reference of the shim, then we 383 // want to have it proxy calls to the real SDK once we've unshimmed. 384 function ensureProxiedToUnshimmed(obj) { 385 const shim = {}; 386 for (const key in obj) { 387 const value = obj[key]; 388 if (typeof value === "function") { 389 shim[key] = function () { 390 if (haveUnshimmed) { 391 return window.FB[key].apply(window.FB, arguments); 392 } 393 return value.apply(this, arguments); 394 }; 395 } else if (typeof value !== "object" || value === null) { 396 shim[key] = value; 397 } else { 398 shim[key] = ensureProxiedToUnshimmed(value); 399 } 400 } 401 return new Proxy(shim, { 402 get: (shimmed, key) => (haveUnshimmed ? window.FB : shimmed)[key], 403 }); 404 } 405 406 window.FB = ensureProxiedToUnshimmed({ 407 api() { 408 loggedGraphApiCalls.push(arguments); 409 }, 410 AppEvents: { 411 activateApp() {}, 412 clearAppVersion() {}, 413 clearUserID() {}, 414 EventNames: { 415 ACHIEVED_LEVEL: "fb_mobile_level_achieved", 416 ADDED_PAYMENT_INFO: "fb_mobile_add_payment_info", 417 ADDED_TO_CART: "fb_mobile_add_to_cart", 418 ADDED_TO_WISHLIST: "fb_mobile_add_to_wishlist", 419 COMPLETED_REGISTRATION: "fb_mobile_complete_registration", 420 COMPLETED_TUTORIAL: "fb_mobile_tutorial_completion", 421 INITIATED_CHECKOUT: "fb_mobile_initiated_checkout", 422 PAGE_VIEW: "fb_page_view", 423 RATED: "fb_mobile_rate", 424 SEARCHED: "fb_mobile_search", 425 SPENT_CREDITS: "fb_mobile_spent_credits", 426 UNLOCKED_ACHIEVEMENT: "fb_mobile_achievement_unlocked", 427 VIEWED_CONTENT: "fb_mobile_content_view", 428 }, 429 getAppVersion: () => "", 430 getUserID: () => "", 431 logEvent() {}, 432 logPageView() {}, 433 logPurchase() {}, 434 ParameterNames: { 435 APP_USER_ID: "_app_user_id", 436 APP_VERSION: "_appVersion", 437 CONTENT_ID: "fb_content_id", 438 CONTENT_TYPE: "fb_content_type", 439 CURRENCY: "fb_currency", 440 DESCRIPTION: "fb_description", 441 LEVEL: "fb_level", 442 MAX_RATING_VALUE: "fb_max_rating_value", 443 NUM_ITEMS: "fb_num_items", 444 PAYMENT_INFO_AVAILABLE: "fb_payment_info_available", 445 REGISTRATION_METHOD: "fb_registration_method", 446 SEARCH_STRING: "fb_search_string", 447 SUCCESS: "fb_success", 448 }, 449 setAppVersion() {}, 450 setUserID() {}, 451 updateUserProperties() {}, 452 }, 453 Canvas: { 454 getHash: () => "", 455 getPageInfo(cb) { 456 cb?.call(this, { 457 clientHeight: 1, 458 clientWidth: 1, 459 offsetLeft: 0, 460 offsetTop: 0, 461 scrollLeft: 0, 462 scrollTop: 0, 463 }); 464 }, 465 Plugin: { 466 hidePluginElement() {}, 467 showPluginElement() {}, 468 }, 469 Prefetcher: { 470 COLLECT_AUTOMATIC: 0, 471 COLLECT_MANUAL: 1, 472 addStaticResource() {}, 473 setCollectionMode() {}, 474 }, 475 scrollTo() {}, 476 setAutoGrow() {}, 477 setDoneLoading() {}, 478 setHash() {}, 479 setSize() {}, 480 setUrlHandler() {}, 481 startTimer() {}, 482 stopTimer() {}, 483 }, 484 Event: { 485 subscribe(e, f) { 486 if (!eventHandlers.has(e)) { 487 eventHandlers.set(e, new Set()); 488 } 489 eventHandlers.get(e).add(f); 490 }, 491 unsubscribe(e, f) { 492 eventHandlers.get(e)?.delete(f); 493 }, 494 }, 495 frictionless: { 496 init() {}, 497 isAllowed: () => false, 498 }, 499 gamingservices: { 500 friendFinder() {}, 501 uploadImageToMediaLibrary() {}, 502 }, 503 getAccessToken: () => null, 504 getAuthResponse() { 505 return { status: "" }; 506 }, 507 getLoginStatus(cb) { 508 cb?.call(this, { status: "unknown" }); 509 }, 510 getUserID() {}, 511 init(_initInfo) { 512 initInfo = _initInfo; // in case the site is not using fbAsyncInit 513 }, 514 login(cb, opts) { 515 // We have to load Facebook's script, and then wait for it to call 516 // window.open. By that time, the popup blocker will likely trigger. 517 // So we open a popup now with about:blank, and then make sure FB 518 // will re-use that same popup later. 519 if (needPopup) { 520 activePopup = window.open("about:blank", popupName, buildPopupParams()); 521 } 522 allowFacebookSDK(() => { 523 activePopup = undefined; 524 function runPostLoginCallbacks() { 525 try { 526 cb?.apply(this, arguments); 527 } catch (e) { 528 console.error(e); 529 } 530 if (activeOnloginAttribute) { 531 setTimeout(activeOnloginAttribute, 1); 532 activeOnloginAttribute = undefined; 533 } 534 } 535 window.FB.login(runPostLoginCallbacks, opts); 536 }).catch(() => { 537 activePopup = undefined; 538 activeOnloginAttribute = undefined; 539 try { 540 cb?.({}); 541 } catch (e) { 542 console.error(e); 543 } 544 }); 545 }, 546 logout(cb) { 547 cb?.call(this); 548 }, 549 ui(params, fn) { 550 if (params.method === "permissions.oauth") { 551 window.FB.login(fn, params); 552 } 553 }, 554 XFBML: { 555 parse(node, cb) { 556 node = node || document; 557 node.querySelectorAll(".fb-login-button").forEach(makeLoginPlaceholder); 558 node.querySelectorAll(".fb-video").forEach(makeVideoPlaceholder); 559 try { 560 cb?.call(this); 561 } catch (e) { 562 console.error(e); 563 } 564 }, 565 }, 566 __buffer: { 567 replay: null, 568 calls: [], 569 opts: null, 570 }, 571 }); 572 573 window.FB.XFBML.parse(); 574 575 window?.fbAsyncInit?.(); 576 }