tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }