tor-browser

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

event-breakpoints.js (15848B)


      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 *
      9 * @param {string} groupID
     10 * @param {string} eventType
     11 * @param {Function} condition: Optional function that takes a Window as parameter. When
     12 *                   passed, the event will only be included if the result of the function
     13 *                   call is `true` (See `getAvailableEventBreakpoints`).
     14 * @returns {object}
     15 */
     16 function generalEvent(groupID, eventType, condition) {
     17  return {
     18    id: `event.${groupID}.${eventType}`,
     19    type: "event",
     20    name: eventType,
     21    message: `DOM '${eventType}' event`,
     22    eventType,
     23    // DOM Events which may fire on the global object, or on DOM Elements
     24    targetTypes: ["global", "node"],
     25    condition,
     26  };
     27 }
     28 function nodeEvent(groupID, eventType) {
     29  return {
     30    ...generalEvent(groupID, eventType),
     31    targetTypes: ["node"],
     32  };
     33 }
     34 function mediaNodeEvent(groupID, eventType) {
     35  return {
     36    ...generalEvent(groupID, eventType),
     37    targetTypes: ["node"],
     38 
     39    // Media events need some specific handling in `eventBreakpointForNotification()`
     40    // to ensure that the event is fired on either <video> or <audio> tags.
     41    isMediaEvent: true,
     42  };
     43 }
     44 function globalEvent(groupID, eventType) {
     45  return {
     46    ...generalEvent(groupID, eventType),
     47    message: `Global '${eventType}' event`,
     48    // DOM Events which are only fired on the global object
     49    targetTypes: ["global"],
     50  };
     51 }
     52 function xhrEvent(groupID, eventType) {
     53  return {
     54    ...generalEvent(groupID, eventType),
     55    message: `XHR '${eventType}' event`,
     56    targetTypes: ["xhr"],
     57  };
     58 }
     59 
     60 function closeWatcherEvent(groupID, eventType) {
     61  return {
     62    ...generalEvent(groupID, eventType),
     63    message: `CloseWatcher '${eventType}' event`,
     64    targetTypes: ["closewatcher"],
     65  };
     66 }
     67 
     68 function webSocketEvent(groupID, eventType) {
     69  return {
     70    ...generalEvent(groupID, eventType),
     71    message: `WebSocket '${eventType}' event`,
     72    targetTypes: ["websocket"],
     73  };
     74 }
     75 
     76 function workerEvent(eventType) {
     77  return {
     78    ...generalEvent("worker", eventType),
     79    message: `Worker '${eventType}' event`,
     80    targetTypes: ["worker"],
     81  };
     82 }
     83 
     84 function timerEvent(type, operation, name, notificationType) {
     85  return {
     86    id: `timer.${type}.${operation}`,
     87    type: "simple",
     88    name,
     89    message: name,
     90    notificationType,
     91  };
     92 }
     93 
     94 function animationEvent(operation, name, notificationType) {
     95  return {
     96    id: `animationframe.${operation}`,
     97    type: "simple",
     98    name,
     99    message: name,
    100    notificationType,
    101  };
    102 }
    103 
    104 const SCRIPT_FIRST_STATEMENT_BREAKPOINT = {
    105  id: "script.source.firstStatement",
    106  type: "script",
    107  name: "Script First Statement",
    108  message: "Script First Statement",
    109 };
    110 
    111 const AVAILABLE_BREAKPOINTS = [
    112  {
    113    name: "Animation",
    114    items: [
    115      animationEvent(
    116        "request",
    117        "Request Animation Frame",
    118        "requestAnimationFrame"
    119      ),
    120      animationEvent(
    121        "cancel",
    122        "Cancel Animation Frame",
    123        "cancelAnimationFrame"
    124      ),
    125      animationEvent(
    126        "fire",
    127        "Animation Frame fired",
    128        "requestAnimationFrameCallback"
    129      ),
    130    ],
    131  },
    132  {
    133    name: "Clipboard",
    134    items: [
    135      generalEvent("clipboard", "copy"),
    136      generalEvent("clipboard", "cut"),
    137      generalEvent("clipboard", "paste"),
    138      generalEvent("clipboard", "beforecopy"),
    139      generalEvent("clipboard", "beforecut"),
    140      generalEvent("clipboard", "beforepaste"),
    141    ],
    142  },
    143  {
    144    name: "CloseWatcher",
    145    items: [
    146      closeWatcherEvent("closewatcher", "cancel", () =>
    147        Services.prefs.getBoolPref("dom.closewatcher.enabled")
    148      ),
    149      closeWatcherEvent("closewatcher", "close", () =>
    150        Services.prefs.getBoolPref("dom.closewatcher.enabled")
    151      ),
    152    ],
    153  },
    154  {
    155    name: "Control",
    156    items: [
    157      generalEvent("control", "beforetoggle"),
    158      generalEvent("control", "blur"),
    159      generalEvent("control", "change"),
    160      generalEvent("control", "focus"),
    161      generalEvent("control", "focusin"),
    162      generalEvent("control", "focusout"),
    163      // The condition should be removed when "dom.element.commandfor.enabled" is removed
    164      generalEvent(
    165        "control",
    166        "command",
    167        global => global && "CommandEvent" in global
    168      ),
    169      generalEvent("control", "reset"),
    170      generalEvent("control", "resize"),
    171      generalEvent("control", "scroll"),
    172      generalEvent("control", "scrollend"),
    173      generalEvent("control", "select"),
    174      generalEvent("control", "toggle"),
    175      generalEvent("control", "submit"),
    176      generalEvent("control", "zoom"),
    177    ],
    178  },
    179  {
    180    name: "DOM Mutation",
    181    items: [
    182      // Deprecated DOM events.
    183      nodeEvent("dom-mutation", "DOMActivate"),
    184      nodeEvent("dom-mutation", "DOMFocusIn"),
    185      nodeEvent("dom-mutation", "DOMFocusOut"),
    186 
    187      // DOM load events.
    188      nodeEvent("dom-mutation", "DOMContentLoaded"),
    189    ],
    190  },
    191  {
    192    name: "Device",
    193    items: [
    194      globalEvent("device", "deviceorientation"),
    195      globalEvent("device", "devicemotion"),
    196    ],
    197  },
    198  {
    199    name: "Drag and Drop",
    200    items: [
    201      generalEvent("drag-and-drop", "drag"),
    202      generalEvent("drag-and-drop", "dragstart"),
    203      generalEvent("drag-and-drop", "dragend"),
    204      generalEvent("drag-and-drop", "dragenter"),
    205      generalEvent("drag-and-drop", "dragover"),
    206      generalEvent("drag-and-drop", "dragleave"),
    207      generalEvent("drag-and-drop", "drop"),
    208    ],
    209  },
    210  {
    211    name: "Keyboard",
    212    items: [
    213      generalEvent("keyboard", "beforeinput"),
    214      generalEvent("keyboard", "input"),
    215      generalEvent("keyboard", "textInput", () =>
    216        // Services.prefs isn't available on worker targets
    217        Services.prefs?.getBoolPref("dom.events.textevent.enabled")
    218      ),
    219      generalEvent("keyboard", "keydown"),
    220      generalEvent("keyboard", "keyup"),
    221      generalEvent("keyboard", "keypress"),
    222      generalEvent("keyboard", "compositionstart"),
    223      generalEvent("keyboard", "compositionupdate"),
    224      generalEvent("keyboard", "compositionend"),
    225    ].filter(Boolean),
    226  },
    227  {
    228    name: "Load",
    229    items: [
    230      globalEvent("load", "load"),
    231      globalEvent("load", "beforeunload"),
    232      globalEvent("load", "unload"),
    233      globalEvent("load", "abort"),
    234      globalEvent("load", "error"),
    235      globalEvent("load", "hashchange"),
    236      globalEvent("load", "popstate"),
    237    ],
    238  },
    239  {
    240    name: "Media",
    241    items: [
    242      mediaNodeEvent("media", "play"),
    243      mediaNodeEvent("media", "pause"),
    244      mediaNodeEvent("media", "playing"),
    245      mediaNodeEvent("media", "canplay"),
    246      mediaNodeEvent("media", "canplaythrough"),
    247      mediaNodeEvent("media", "seeking"),
    248      mediaNodeEvent("media", "seeked"),
    249      mediaNodeEvent("media", "timeupdate"),
    250      mediaNodeEvent("media", "ended"),
    251      mediaNodeEvent("media", "ratechange"),
    252      mediaNodeEvent("media", "durationchange"),
    253      mediaNodeEvent("media", "volumechange"),
    254      mediaNodeEvent("media", "loadstart"),
    255      mediaNodeEvent("media", "progress"),
    256      mediaNodeEvent("media", "suspend"),
    257      mediaNodeEvent("media", "abort"),
    258      mediaNodeEvent("media", "error"),
    259      mediaNodeEvent("media", "emptied"),
    260      mediaNodeEvent("media", "stalled"),
    261      mediaNodeEvent("media", "loadedmetadata"),
    262      mediaNodeEvent("media", "loadeddata"),
    263      mediaNodeEvent("media", "waiting"),
    264    ],
    265  },
    266  {
    267    name: "Mouse",
    268    items: [
    269      generalEvent("mouse", "auxclick"),
    270      generalEvent("mouse", "click"),
    271      generalEvent("mouse", "dblclick"),
    272      generalEvent("mouse", "mousedown"),
    273      generalEvent("mouse", "mouseup"),
    274      generalEvent("mouse", "mouseover"),
    275      generalEvent("mouse", "mousemove"),
    276      generalEvent("mouse", "mouseout"),
    277      generalEvent("mouse", "mouseenter"),
    278      generalEvent("mouse", "mouseleave"),
    279      generalEvent("mouse", "mousewheel"),
    280      generalEvent("mouse", "wheel"),
    281      generalEvent("mouse", "contextmenu"),
    282    ],
    283  },
    284  {
    285    name: "Pointer",
    286    items: [
    287      generalEvent("pointer", "pointerover"),
    288      generalEvent("pointer", "pointerout"),
    289      generalEvent("pointer", "pointerenter"),
    290      generalEvent("pointer", "pointerleave"),
    291      generalEvent("pointer", "pointerdown"),
    292      generalEvent("pointer", "pointerup"),
    293      generalEvent("pointer", "pointermove"),
    294      generalEvent("pointer", "pointercancel"),
    295      generalEvent("pointer", "pointerrawupdate"),
    296      generalEvent("pointer", "gotpointercapture"),
    297      generalEvent("pointer", "lostpointercapture"),
    298    ],
    299  },
    300  {
    301    name: "Script",
    302    items: [SCRIPT_FIRST_STATEMENT_BREAKPOINT],
    303  },
    304  {
    305    name: "Timer",
    306    items: [
    307      timerEvent("timeout", "set", "setTimeout", "setTimeout"),
    308      timerEvent("timeout", "clear", "clearTimeout", "clearTimeout"),
    309      timerEvent("timeout", "fire", "setTimeout fired", "setTimeoutCallback"),
    310      timerEvent("interval", "set", "setInterval", "setInterval"),
    311      timerEvent("interval", "clear", "clearInterval", "clearInterval"),
    312      timerEvent(
    313        "interval",
    314        "fire",
    315        "setInterval fired",
    316        "setIntervalCallback"
    317      ),
    318    ],
    319  },
    320  {
    321    name: "Touch",
    322    items: [
    323      generalEvent("touch", "touchstart"),
    324      generalEvent("touch", "touchmove"),
    325      generalEvent("touch", "touchend"),
    326      generalEvent("touch", "touchcancel"),
    327    ],
    328  },
    329  {
    330    name: "WebSocket",
    331    items: [
    332      webSocketEvent("websocket", "open"),
    333      webSocketEvent("websocket", "message"),
    334      webSocketEvent("websocket", "error"),
    335      webSocketEvent("websocket", "close"),
    336    ],
    337  },
    338  {
    339    name: "Worker",
    340    items: [
    341      workerEvent("message"),
    342      workerEvent("messageerror"),
    343 
    344      // Service Worker events.
    345      globalEvent("serviceworker", "fetch"),
    346    ],
    347  },
    348  {
    349    name: "XHR",
    350    items: [
    351      xhrEvent("xhr", "readystatechange"),
    352      xhrEvent("xhr", "load"),
    353      xhrEvent("xhr", "loadstart"),
    354      xhrEvent("xhr", "loadend"),
    355      xhrEvent("xhr", "abort"),
    356      xhrEvent("xhr", "error"),
    357      xhrEvent("xhr", "progress"),
    358      xhrEvent("xhr", "timeout"),
    359    ],
    360  },
    361 ];
    362 
    363 const FLAT_EVENTS = [];
    364 for (const category of AVAILABLE_BREAKPOINTS) {
    365  for (const event of category.items) {
    366    FLAT_EVENTS.push(event);
    367  }
    368 }
    369 const EVENTS_BY_ID = {};
    370 for (const event of FLAT_EVENTS) {
    371  if (EVENTS_BY_ID[event.id]) {
    372    throw new Error("Duplicate event ID detected: " + event.id);
    373  }
    374  EVENTS_BY_ID[event.id] = event;
    375 }
    376 
    377 const SIMPLE_EVENTS = {};
    378 const DOM_EVENTS = {};
    379 for (const eventBP of FLAT_EVENTS) {
    380  if (eventBP.type === "simple") {
    381    const { notificationType } = eventBP;
    382    if (SIMPLE_EVENTS[notificationType]) {
    383      throw new Error("Duplicate simple event");
    384    }
    385    SIMPLE_EVENTS[notificationType] = eventBP.id;
    386  } else if (eventBP.type === "event") {
    387    const { eventType, targetTypes } = eventBP;
    388 
    389    if (!Array.isArray(targetTypes) || !targetTypes.length) {
    390      throw new Error("Expect a targetTypes array for each event definition");
    391    }
    392 
    393    for (const targetType of targetTypes) {
    394      let byEventType = DOM_EVENTS[targetType];
    395      if (!byEventType) {
    396        byEventType = {};
    397        DOM_EVENTS[targetType] = byEventType;
    398      }
    399 
    400      if (byEventType[eventType]) {
    401        throw new Error("Duplicate dom event: " + eventType);
    402      }
    403      byEventType[eventType] = eventBP.id;
    404    }
    405  } else if (eventBP.type === "script") {
    406    // Nothing to do.
    407  } else {
    408    throw new Error("Unknown type: " + eventBP.type);
    409  }
    410 }
    411 
    412 exports.eventBreakpointForNotification = eventBreakpointForNotification;
    413 function eventBreakpointForNotification(dbg, notification) {
    414  const notificationType = notification.type;
    415 
    416  if (notification.type === "domEvent") {
    417    const domEventNotification = DOM_EVENTS[notification.targetType];
    418    if (!domEventNotification) {
    419      return null;
    420    }
    421 
    422    // The 'event' value is a cross-compartment wrapper for the DOM Event object.
    423    // While we could use that directly in the main thread as an Xray wrapper,
    424    // when debugging workers we can't, because it is an opaque wrapper.
    425    // To make things work, we have to always interact with the Event object via
    426    // the Debugger.Object interface.
    427    const evt = dbg
    428      .makeGlobalObjectReference(notification.global)
    429      .makeDebuggeeValue(notification.event);
    430 
    431    const eventType = evt.getProperty("type").return;
    432    const id = domEventNotification[eventType];
    433    if (!id) {
    434      return null;
    435    }
    436    const eventBreakpoint = EVENTS_BY_ID[id];
    437 
    438    // Does some additional checks for media events to ensure the DOM Event
    439    // was fired on either <audio> or <video> tags.
    440    if (eventBreakpoint.isMediaEvent) {
    441      const currentTarget = evt.getProperty("currentTarget").return;
    442      if (!currentTarget) {
    443        return null;
    444      }
    445 
    446      const nodeType = currentTarget.getProperty("nodeType").return;
    447      const namespaceURI = currentTarget.getProperty("namespaceURI").return;
    448      if (
    449        nodeType !== 1 /* ELEMENT_NODE */ ||
    450        namespaceURI !== "http://www.w3.org/1999/xhtml"
    451      ) {
    452        return null;
    453      }
    454 
    455      const nodeName = currentTarget
    456        .getProperty("nodeName")
    457        .return.toLowerCase();
    458      if (nodeName !== "audio" && nodeName !== "video") {
    459        return null;
    460      }
    461    }
    462 
    463    return id;
    464  }
    465 
    466  return SIMPLE_EVENTS[notificationType] || null;
    467 }
    468 
    469 exports.makeEventBreakpointMessage = makeEventBreakpointMessage;
    470 function makeEventBreakpointMessage(id) {
    471  return EVENTS_BY_ID[id].message;
    472 }
    473 
    474 exports.firstStatementBreakpointId = firstStatementBreakpointId;
    475 function firstStatementBreakpointId() {
    476  return SCRIPT_FIRST_STATEMENT_BREAKPOINT.id;
    477 }
    478 
    479 exports.eventsRequireNotifications = eventsRequireNotifications;
    480 function eventsRequireNotifications(ids) {
    481  for (const id of ids) {
    482    const eventBreakpoint = EVENTS_BY_ID[id];
    483 
    484    // Script events are implemented directly in the server and do not require
    485    // notifications from Gecko, so there is no need to watch for them.
    486    if (eventBreakpoint && eventBreakpoint.type !== "script") {
    487      return true;
    488    }
    489  }
    490  return false;
    491 }
    492 
    493 exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints;
    494 /**
    495 * Get all available event breakpoints
    496 *
    497 * @param {Window|WorkerGlobalScope} global
    498 * @returns {Array<object>} An array containing object with a few properties :
    499 *    - {String} id: unique identifier
    500 *    - {String} name: Description for the event to be displayed in UI (no translated)
    501 *    - {String} type: Either "simple" or "event"
    502 *    Only for type="simple":
    503 *    - {String} notificationType: platform name of the event
    504 *    Only for type="event":
    505 *    - {String} eventType: platform name of the event
    506 *    - {Array<String>} targetTypes: List of potential target on which the event is fired.
    507 *                                   Can be "global", "node", "xhr", "worker",...
    508 */
    509 function getAvailableEventBreakpoints(global) {
    510  const available = [];
    511  for (const { name, items } of AVAILABLE_BREAKPOINTS) {
    512    available.push({
    513      name,
    514      events: items
    515        .filter(item => !item.condition || item.condition(global))
    516        .map(item => ({
    517          id: item.id,
    518 
    519          // The name to be displayed in UI
    520          name: item.name,
    521 
    522          // The type of event: either simple or event
    523          type: item.type,
    524 
    525          // For type=simple
    526          notificationType: item.notificationType,
    527 
    528          // For type=event
    529          eventType: item.eventType,
    530          targetTypes: item.targetTypes,
    531        })),
    532    });
    533  }
    534  return available;
    535 }
    536 exports.validateEventBreakpoint = validateEventBreakpoint;
    537 function validateEventBreakpoint(id) {
    538  return !!EVENTS_BY_ID[id];
    539 }