Bug 1713710 - Add a SmartBlock shim for Vidible-served video playback on user opt-in; r=denschub,webcompat-reviewers
authorThomas Wisniewski <twisniewski@mozilla.com>
Mon, 16 Aug 2021 19:46:19 +0000
changeset 589048 2d6e47d31b396955cafeac86a4324374ee292e75
parent 589047 0bf4f55954f36b584d34d7754619f5d7ff914f60
child 589049 d6f3465132d38a99c2c69ddf0e8ea79f5d397eea
push id38711
push usermalexandru@mozilla.com
push dateTue, 17 Aug 2021 03:38:20 +0000
treeherdermozilla-central@6613af6e3203 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdenschub, webcompat-reviewers
bugs1713710
milestone93.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1713710 - Add a SmartBlock shim for Vidible-served video playback on user opt-in; r=denschub,webcompat-reviewers Differential Revision: https://phabricator.services.mozilla.com/D122756
browser/extensions/webcompat/data/shims.js
browser/extensions/webcompat/manifest.json
browser/extensions/webcompat/moz.build
browser/extensions/webcompat/shims/vidible.js
--- a/browser/extensions/webcompat/data/shims.js
+++ b/browser/extensions/webcompat/data/shims.js
@@ -412,11 +412,37 @@ const AVAILABLE_SHIMS = [
     id: "RichRelevance",
     platform: "all",
     name: "Rich Relevance",
     bug: "1713725",
     file: "rich-relevance.js",
     matches: ["*://media.richrelevance.com/rrserver/js/1.2/p13n.js"],
     onlyIfBlockedByETP: true,
   },
+  {
+    id: "Vidible",
+    branch: ["nightly"],
+    platform: "all",
+    name: "Vidible",
+    bug: "1713710",
+    file: "vidible.js",
+    logos: ["play.svg"],
+    matches: [
+      "*://*.vidible.tv/*/vidible-min.js*",
+      "*://vdb-cdn-files.s3.amazonaws.com/*/vidible-min.js*",
+    ],
+    needsShimHelpers: ["optIn"],
+    onlyIfBlockedByETP: true,
+    unblocksOnOptIn: [
+      "*://delivery.vidible.tv/jsonp/pid=*/vid=*/*.js*",
+      "*://delivery.vidible.tv/placement/*",
+      "*://img.vidible.tv/prod/*",
+      "*://cdn-ssl.vidible.tv/prod/player/js/*.js",
+      "*://hlsrv.vidible.tv/prod/*.m3u8*",
+      "*://videos.vidible.tv/prod/*.key*",
+      "*://videos.vidible.tv/prod/*.mp4*",
+      "*://videos.vidible.tv/prod/*.webm*",
+      "*://videos.vidible.tv/prod/*.ts*",
+    ],
+  },
 ];
 
 module.exports = AVAILABLE_SHIMS;
--- a/browser/extensions/webcompat/manifest.json
+++ b/browser/extensions/webcompat/manifest.json
@@ -1,13 +1,13 @@
 {
   "manifest_version": 2,
   "name": "Web Compatibility Interventions",
   "description": "Urgent post-release fixes for web compatibility.",
-  "version": "25.4.0",
+  "version": "25.5.0",
 
   "applications": {
     "gecko": {
       "id": "webcompat@mozilla.org",
       "strict_min_version": "59.0b5"
     }
   },
 
@@ -119,11 +119,12 @@
     "shims/mochitest-shim-3.js",
     "shims/optimizely.js",
     "shims/play.svg",
     "shims/rambler-authenticator.js",
     "shims/rich-relevance.js",
     "shims/tracking-pixel.png",
     "shims/vast2.xml",
     "shims/vast3.xml",
+    "shims/vidible.js",
     "shims/vmad.xml"
   ]
 }
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -108,16 +108,17 @@ FINAL_TARGET_FILES.features["webcompat@m
     "shims/mochitest-shim-3.js",
     "shims/optimizely.js",
     "shims/play.svg",
     "shims/rambler-authenticator.js",
     "shims/rich-relevance.js",
     "shims/tracking-pixel.png",
     "shims/vast2.xml",
     "shims/vast3.xml",
+    "shims/vidible.js",
     "shims/vmad.xml",
 ]
 
 FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["lib"] += [
     "lib/about_compat_broker.js",
     "lib/custom_functions.js",
     "lib/injections.js",
     "lib/intervention_helpers.js",
new file mode 100644
--- /dev/null
+++ b/browser/extensions/webcompat/shims/vidible.js
@@ -0,0 +1,424 @@
+/* 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 1713710 - Shim Vidible video player
+ *
+ * Sites relying on Vidible's video player may experience broken videos if that
+ * script is blocked. This shim allows users to opt into viewing those videos
+ * regardless of any tracking consequences, by providing placeholders for each.
+ */
+
+if (!window.vidible?.version) {
+  const PlayIconURL = "https://smartblock.firefox.etp/play.svg";
+
+  const originalScript = document.currentScript.src;
+
+  const getGUID = () => {
+    const v = crypto.getRandomValues(new Uint8Array(20));
+    return Array.from(v, c => c.toString(16)).join("");
+  };
+
+  const sendMessageToAddon = (function() {
+    const shimId = "Vidible";
+    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 Shimmer = (function() {
+    // If a page might store references to an object before we replace it,
+    // ensure that it only receives proxies to that object created by
+    // `Shimmer.proxy(obj)`. Later when the unshimmed object is created,
+    // call `Shimmer.unshim(proxy, unshimmed)`. This way the references
+    // will automatically "become" the unshimmed object when appropriate.
+
+    const shimmedObjects = new WeakMap();
+    const unshimmedObjects = new Map();
+
+    function proxy(shim) {
+      if (shimmedObjects.has(shim)) {
+        return shimmedObjects.get(shim);
+      }
+
+      const prox = new Proxy(shim, {
+        get: (target, k) => {
+          if (unshimmedObjects.has(prox)) {
+            return unshimmedObjects.get(prox)[k];
+          }
+          return target[k];
+        },
+        apply: (target, thisArg, args) => {
+          if (unshimmedObjects.has(prox)) {
+            return unshimmedObjects.get(prox)(...args);
+          }
+          return target.apply(thisArg, args);
+        },
+        construct: (target, args) => {
+          if (unshimmedObjects.has(prox)) {
+            return new unshimmedObjects.get(prox)(...args);
+          }
+          return new target(...args);
+        },
+      });
+      shimmedObjects.set(shim, prox);
+      shimmedObjects.set(prox, prox);
+
+      for (const key in shim) {
+        const value = shim[key];
+        if (typeof value === "function") {
+          shim[key] = function() {
+            const unshimmed = unshimmedObjects.get(prox);
+            if (unshimmed) {
+              return unshimmed[key].apply(unshimmed, arguments);
+            }
+            return value.apply(this, arguments);
+          };
+        } else if (typeof value !== "object" || value === null) {
+          shim[key] = value;
+        } else {
+          shim[key] = Shimmer.proxy(value);
+        }
+      }
+
+      return prox;
+    }
+
+    function unshim(shim, unshimmed) {
+      unshimmedObjects.set(shim, unshimmed);
+
+      for (const prop in shim) {
+        if (prop in unshimmed) {
+          const un = unshimmed[prop];
+          if (typeof un === "object" && un !== null) {
+            unshim(shim[prop], un);
+          }
+        } else {
+          unshimmedObjects.set(shim[prop], undefined);
+        }
+      }
+    }
+
+    return { proxy, unshim };
+  })();
+
+  const extras = [];
+  const playersByNode = new WeakMap();
+  const playerData = new Map();
+
+  const getJSONPVideoPlacements = () => {
+    return document.querySelectorAll(
+      `script[src*="delivery.vidible.tv/jsonp"]`
+    );
+  };
+
+  const allowVidible = () => {
+    if (allowVidible.promise) {
+      return allowVidible.promise;
+    }
+
+    const shim = window.vidible;
+    window.vidible = undefined;
+
+    allowVidible.promise = sendMessageToAddon("optIn")
+      .then(() => {
+        return new Promise((resolve, reject) => {
+          const script = document.createElement("script");
+          script.src = originalScript;
+          script.addEventListener("load", () => {
+            Shimmer.unshim(shim, window.vidible);
+
+            for (const args of extras) {
+              window.visible.registerExtra(...args);
+            }
+
+            for (const jsonp of getJSONPVideoPlacements()) {
+              const { src } = jsonp;
+              const jscript = document.createElement("script");
+              jscript.onload = resolve;
+              jscript.src = src;
+              jsonp.replaceWith(jscript);
+            }
+
+            for (const [playerShim, data] of playerData.entries()) {
+              const { loadCalled, on, parent, placeholder, setup } = data;
+
+              placeholder?.remove();
+
+              const player = window.vidible.player(parent);
+              Shimmer.unshim(playerShim, player);
+
+              for (const [type, fns] of on.entries()) {
+                for (const fn of fns) {
+                  try {
+                    player.on(type, fn);
+                  } catch (e) {
+                    console.error(e);
+                  }
+                }
+              }
+
+              if (setup) {
+                player.setup(setup);
+              }
+
+              if (loadCalled) {
+                player.load();
+              }
+            }
+
+            resolve();
+          });
+
+          script.addEventListener("error", () => {
+            script.remove();
+            reject();
+          });
+
+          document.head.appendChild(script);
+        });
+      })
+      .catch(() => {
+        window.vidible = shim;
+        delete allowVidible.promise;
+      });
+
+    return allowVidible.promise;
+  };
+
+  const createVideoPlaceholder = (service, callback) => {
+    const placeholder = document.createElement("div");
+    placeholder.style = `
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      min-width: 160px;
+      min-height: 100px;
+      top: 0px;
+      left: 0px;
+      background: #000;
+      color: #fff;
+      text-align: center;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background-image: url(${PlayIconURL});
+      background-position: 50% 47.5%;
+      background-repeat: no-repeat;
+      background-size: 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 ${service} content`;
+    placeholder.addEventListener("click", evt => {
+      evt.isTrusted && callback();
+    });
+    return placeholder;
+  };
+
+  const Player = function(parent) {
+    const existing = playersByNode.get(parent);
+    if (existing) {
+      return existing;
+    }
+
+    const player = Shimmer.proxy(this);
+    playersByNode.set(parent, player);
+
+    const placeholder = createVideoPlaceholder("Vidible", allowVidible);
+    parent.parentNode.insertBefore(placeholder, parent);
+
+    playerData.set(player, {
+      on: new Map(),
+      parent,
+      placeholder,
+    });
+    return player;
+  };
+
+  const changeData = function(fn) {
+    const data = playerData.get(this);
+    if (data) {
+      fn(data);
+      playerData.set(this, data);
+    }
+  };
+
+  Player.prototype = {
+    addEventListener() {},
+    destroy() {
+      const { placeholder } = playerData.get(this);
+      placeholder?.remove();
+      playerData.delete(this);
+    },
+    dispatchEvent() {},
+    getAdsPassedTime() {},
+    getAllMacros() {},
+    getCurrentTime() {},
+    getDuration() {},
+    getHeight() {},
+    getPixelsLog() {},
+    getPlayerContainer() {},
+    getPlayerInfo() {},
+    getPlayerStatus() {},
+    getRequestsLog() {},
+    getStripUrl() {},
+    getVolume() {},
+    getWidth() {},
+    hidePlayReplayControls() {},
+    isMuted() {},
+    isPlaying() {},
+    load() {
+      changeData(data => (data.loadCalled = true));
+    },
+    mute() {},
+    on(type, fn) {
+      changeData(({ on }) => {
+        if (!on.has(type)) {
+          on.set(type, new Set());
+        }
+        on.get(type).add(fn);
+      });
+    },
+    off(type, fn) {
+      changeData(({ on }) => {
+        on.get(type)?.delete(fn);
+      });
+    },
+    overrideMacro() {},
+    pause() {},
+    play() {},
+    playVideoByIndex() {},
+    removeEventListener() {},
+    seekTo() {},
+    sendBirthDate() {},
+    sendKey() {},
+    setup(s) {
+      changeData(data => (data.setup = s));
+      return this;
+    },
+    setVideosToPlay() {},
+    setVolume() {},
+    showPlayReplayControls() {},
+    toggleFullscreen() {},
+    toggleMute() {},
+    togglePlay() {},
+    updateBid() {},
+    version() {},
+    volume() {},
+  };
+
+  const vidible = {
+    ADVERT_CLOSED: "advertClosed",
+    AD_END: "adend",
+    AD_META: "admeta",
+    AD_PAUSED: "adpaused",
+    AD_PLAY: "adplay",
+    AD_START: "adstart",
+    AD_TIMEUPDATE: "adtimeupdate",
+    AD_WAITING: "adwaiting",
+    AGE_GATE_DISPLAYED: "agegatedisplayed",
+    BID_UPDATED: "BidUpdated",
+    CAROUSEL_CLICK: "CarouselClick",
+    CONTEXT_ENDED: "contextended",
+    CONTEXT_STARTED: "contextstarted",
+    ENTER_FULLSCREEN: "playerenterfullscreen",
+    EXIT_FULLSCREEN: "playerexitfullscreen",
+    FALLBACK: "fallback",
+    FLOAT_END_ACTION: "floatended",
+    FLOAT_START_ACTION: "floatstarted",
+    HIDE_PLAY_REPLAY_BUTTON: "hideplayreplaybutton",
+    LIGHTBOX_ACTIVATED: "lightboxactivated",
+    LIGHTBOX_DEACTIVATED: "lightboxdeactivated",
+    MUTE: "Mute",
+    PLAYER_CONTROLS_STATE_CHANGE: "playercontrolsstatechaned",
+    PLAYER_DOCKED: "playerDocked",
+    PLAYER_ERROR: "playererror",
+    PLAYER_FLOATING: "playerFloating",
+    PLAYER_READY: "playerready",
+    PLAYER_RESIZE: "playerresize",
+    PLAYLIST_END: "playlistend",
+    SEEK_END: "SeekEnd",
+    SEEK_START: "SeekStart",
+    SHARE_SCREEN_CLOSED: "sharescreenclosed",
+    SHARE_SCREEN_OPENED: "sharescreenopened",
+    SHOW_PLAY_REPLAY_BUTTON: "showplayreplaybutton",
+    SUBTITLES_DISABLED: "subtitlesdisabled",
+    SUBTITLES_ENABLED: "subtitlesenabled",
+    SUBTITLES_READY: "subtitlesready",
+    UNMUTE: "Unmute",
+    VIDEO_DATA_LOADED: "videodataloaded",
+    VIDEO_END: "videoend",
+    VIDEO_META: "videometadata",
+    VIDEO_MODULE_CREATED: "videomodulecreated",
+    VIDEO_PAUSE: "videopause",
+    VIDEO_PLAY: "videoplay",
+    VIDEO_SEEKEND: "videoseekend",
+    VIDEO_SELECTED: "videoselected",
+    VIDEO_START: "videostart",
+    VIDEO_TIMEUPDATE: "videotimeupdate",
+    VIDEO_VOLUME_CHANGED: "videovolumechanged",
+    VOLUME: "Volume",
+    _getContexts: () => [],
+    "content.CLICK": "content.click",
+    "content.IMPRESSION": "content.impression",
+    "content.QUARTILE": "content.quartile",
+    "content.VIEW": "content.view",
+    createPlayer: parent => new Player(parent),
+    createPlayerAsync: parent => new Player(parent),
+    createVPAIDPlayer: parent => new Player(parent),
+    destroyAll() {},
+    extension() {},
+    getContext() {},
+    player: parent => new Player(parent),
+    playerInceptionTime() {
+      return { undefined: 1620149827713 };
+    },
+    registerExtra(a, b, c) {
+      extras.push([a, b, c]);
+    },
+    version: () => "21.1.313",
+  };
+
+  window.vidible = Shimmer.proxy(vidible);
+
+  for (const jsonp of getJSONPVideoPlacements()) {
+    const player = new Player(jsonp);
+    const { placeholder } = playerData.get(player);
+    jsonp.parentNode.insertBefore(placeholder, jsonp);
+  }
+}