tor-browser

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

SessionData.sys.mjs (16023B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  ContextDescriptorType:
      9    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
     10  Log: "chrome://remote/content/shared/Log.sys.mjs",
     11  RootMessageHandler:
     12    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
     13  WindowGlobalMessageHandler:
     14    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
     15 });
     16 
     17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     18 
     19 /**
     20 * @typedef {string} SessionDataCategory
     21 */
     22 
     23 /**
     24 * Enum of session data categories.
     25 *
     26 * @readonly
     27 * @enum {SessionDataCategory}
     28 */
     29 export const SessionDataCategory = {
     30  Event: "event",
     31  PreloadScript: "preload-script",
     32 };
     33 
     34 /**
     35 * @typedef {string} SessionDataMethod
     36 */
     37 
     38 /**
     39 * Enum of session data methods.
     40 *
     41 * @readonly
     42 * @enum {SessionDataMethod}
     43 */
     44 export const SessionDataMethod = {
     45  Add: "add",
     46  Remove: "remove",
     47 };
     48 
     49 export const SESSION_DATA_SHARED_DATA_KEY = "MessageHandlerSessionData";
     50 
     51 // This is a map from session id to session data, which will be persisted and
     52 // propagated to all processes using Services' sharedData.
     53 // We have to store this as a unique object under a unique shared data key
     54 // because new MessageHandlers in other processes will need to access this data
     55 // without any notion of a specific session.
     56 // This is a singleton.
     57 const sessionDataMap = new Map();
     58 
     59 /**
     60 * @typedef {object} SessionDataItem
     61 * @property {string} moduleName
     62 *     The name of the module responsible for this data item.
     63 * @property {SessionDataCategory} category
     64 *     The category of data. The supported categories depend on the module.
     65 * @property {(string|number|boolean)} value
     66 *     Value of the session data item.
     67 * @property {ContextDescriptor} contextDescriptor
     68 *     ContextDescriptor to which this session data applies.
     69 */
     70 
     71 /**
     72 * @typedef SessionDataItemUpdate
     73 * @property {SessionDataMethod} method
     74 *     The way sessionData is updated.
     75 * @property {string} moduleName
     76 *     The name of the module responsible for this data item.
     77 * @property {SessionDataCategory} category
     78 *     The category of data. The supported categories depend on the module.
     79 * @property {Array<(string|number|boolean)>} values
     80 *     Values of the session data item update.
     81 * @property {ContextDescriptor} contextDescriptor
     82 *     ContextDescriptor to which this session data applies.
     83 */
     84 
     85 /**
     86 * SessionData provides APIs to read and write the session data for a specific
     87 * ROOT message handler. It holds the session data as a property and acts as the
     88 * source of truth for this session data.
     89 *
     90 * The session data of a given message handler network should contain all the
     91 * information that might be needed to setup new contexts, for instance a list
     92 * of subscribed events, a list of breakpoints etc.
     93 *
     94 * The actual session data is an array of SessionDataItems. Example below:
     95 * ```
     96 * data: [
     97 *   {
     98 *     moduleName: "log",
     99 *     category: "event",
    100 *     value: "log.entryAdded",
    101 *     contextDescriptor: { type: "all" }
    102 *   },
    103 *   {
    104 *     moduleName: "browsingContext",
    105 *     category: "event",
    106 *     value: "browsingContext.contextCreated",
    107 *     contextDescriptor: { type: "browser-element", id: "7"}
    108 *   },
    109 *   {
    110 *     moduleName: "browsingContext",
    111 *     category: "event",
    112 *     value: "browsingContext.contextCreated",
    113 *     contextDescriptor: { type: "browser-element", id: "12"}
    114 *   },
    115 * ]
    116 * ```
    117 *
    118 * The session data will be persisted using Services.ppmm.sharedData, so that
    119 * new contexts living in different processes can also access the information
    120 * during their startup.
    121 *
    122 * This class should only be used from a ROOT MessageHandler, or from modules
    123 * owned by a ROOT MessageHandler. Other MessageHandlers should rely on
    124 * SessionDataReader's readSessionData to get read-only access to session data.
    125 *
    126 */
    127 export class SessionData {
    128  #data;
    129  #messageHandler;
    130 
    131  constructor(messageHandler) {
    132    if (messageHandler.constructor.type != lazy.RootMessageHandler.type) {
    133      throw new Error(
    134        "SessionData should only be used from a ROOT MessageHandler"
    135      );
    136    }
    137 
    138    this.#messageHandler = messageHandler;
    139 
    140    /*
    141     * The actual data for this session. This is an array of SessionDataItems.
    142     */
    143    this.#data = [];
    144  }
    145 
    146  destroy() {
    147    // Update the sessionDataMap singleton.
    148    sessionDataMap.delete(this.#messageHandler.sessionId);
    149 
    150    // Update sharedData and flush to force consistency.
    151    Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
    152    Services.ppmm.sharedData.flush();
    153  }
    154 
    155  /**
    156   * Update session data items of a given module, category and
    157   * contextDescriptor.
    158   *
    159   * A SessionDataItem will be added or removed for each value of each update
    160   * in the provided array.
    161   *
    162   * Attempting to add a duplicate SessionDataItem or to remove an unknown
    163   * SessionDataItem will be silently skipped (no-op).
    164   *
    165   * The data will be persisted across processes at the end of this method.
    166   *
    167   * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates
    168   *     Array of session data item updates.
    169   *
    170   * @returns {Array<SessionDataItemUpdate>}
    171   *     The subset of session data item updates which want to be applied.
    172   */
    173  applySessionData(sessionDataItemUpdates = []) {
    174    // The subset of session data item updates, which are cleaned up from
    175    // duplicates and unknown items.
    176    const updates = [];
    177    for (const sessionDataItemUpdate of sessionDataItemUpdates) {
    178      const { category, contextDescriptor, method, moduleName, values } =
    179        sessionDataItemUpdate;
    180      const updatedValues = [];
    181      for (const value of values) {
    182        const item = { moduleName, category, contextDescriptor, value };
    183 
    184        if (method === SessionDataMethod.Add) {
    185          const hasItem = this.#findIndex(item) != -1;
    186 
    187          if (!hasItem) {
    188            this.#data.push(item);
    189            updatedValues.push(value);
    190          } else {
    191            lazy.logger.warn(
    192              `Duplicated session data item was not added: ${JSON.stringify(
    193                item
    194              )}`
    195            );
    196          }
    197        } else {
    198          const itemIndex = this.#findIndex(item);
    199 
    200          if (itemIndex != -1) {
    201            // The item was found in the session data, remove it.
    202            this.#data.splice(itemIndex, 1);
    203            updatedValues.push(value);
    204          } else {
    205            lazy.logger.warn(
    206              `Missing session data item was not removed: ${JSON.stringify(
    207                item
    208              )}`
    209            );
    210          }
    211        }
    212      }
    213 
    214      if (updatedValues.length) {
    215        updates.push({
    216          ...sessionDataItemUpdate,
    217          values: updatedValues,
    218        });
    219      }
    220    }
    221    // Persist the sessionDataMap.
    222    this.#persist();
    223 
    224    return updates;
    225  }
    226 
    227  /**
    228   * Generate session data item update (remove existing items and add new)
    229   * for a given module, category, contextDescriptor and new value.
    230   *
    231   * @param {string} moduleName
    232   *     The name of the module.
    233   * @param {string} category
    234   *     The session data category.
    235   * @param {ContextDescriptor=} contextDescriptor
    236   *     The context descriptor.
    237   * @param {boolean} onlyRemove
    238   *     If it's set to "true" do not add a new session data item.
    239   * @param {(string|number|boolean)} newValue
    240   *     The new value of the session data item.
    241   *
    242   * @returns {Array<SessionDataItemUpdate>} sessionDataItemUpdates
    243   *     Array of session data item updates.
    244   */
    245  generateSessionDataItemUpdate(
    246    moduleName,
    247    category,
    248    contextDescriptor,
    249    onlyRemove,
    250    newValue
    251  ) {
    252    const sessionDataUpdate = [];
    253    const sessionData = this.getSessionData(
    254      moduleName,
    255      category,
    256      contextDescriptor
    257    );
    258 
    259    if (sessionData.length) {
    260      for (const item of sessionData) {
    261        sessionDataUpdate.push({
    262          category,
    263          moduleName,
    264          values: [item.value],
    265          contextDescriptor,
    266          method: SessionDataMethod.Remove,
    267        });
    268      }
    269    }
    270 
    271    if (!onlyRemove) {
    272      sessionDataUpdate.push({
    273        category,
    274        moduleName,
    275        values: [newValue],
    276        contextDescriptor,
    277        method: SessionDataMethod.Add,
    278      });
    279    }
    280 
    281    return sessionDataUpdate;
    282  }
    283 
    284  /**
    285   * Retrieve the SessionDataItems for a given module and type.
    286   *
    287   * @param {string} moduleName
    288   *     The name of the module responsible for this data item.
    289   * @param {string=} category
    290   *     Optional session data category.
    291   * @param {ContextDescriptor=} contextDescriptor
    292   *     Optional context descriptor, to retrieve only session data items added
    293   *     for a specific context descriptor.
    294   * @returns {Array<SessionDataItem>}
    295   *     Array of SessionDataItems for the provided module and type.
    296   */
    297  getSessionData(moduleName, category, contextDescriptor) {
    298    return this.#data.filter(item =>
    299      this.#matchItem(item, moduleName, category, contextDescriptor)
    300    );
    301  }
    302 
    303  /**
    304   * Retrieve the SessionDataItems for a given module, type and
    305   * with context descriptors which would match the provided
    306   * browsing context.
    307   *
    308   * @param {string} moduleName
    309   *     The name of the module.
    310   * @param {string} category
    311   *     The session data category.
    312   * @param {BrowsingContext} context
    313   *     The browsing context.
    314   * @returns {Array<SessionDataItem>}
    315   *     Array of SessionDataItems for the provided module, type
    316   *     and browsing context.
    317   */
    318  getSessionDataForContext(moduleName, category, context) {
    319    return this.#data.filter(
    320      item =>
    321        this.#matchItem(item, moduleName, category) &&
    322        this.#messageHandler.contextMatchesDescriptor(
    323          context,
    324          item.contextDescriptor
    325        )
    326    );
    327  }
    328 
    329  /**
    330   * Checks if any session data exists for a provided module name.
    331   *
    332   * @param {string} moduleName
    333   *     The name of the module responsible for this data item.
    334   * @param {string=} category
    335   *     Optional session data category.
    336   * @param {ContextDescriptor=} contextDescriptor
    337   *     Optional context descriptor, to retrieve only session data items added
    338   *     for a specific context descriptor.
    339   * @returns {boolean}
    340   *     Returns `true` if matching session data is found, `false` otherwise.
    341   */
    342  hasSessionData(moduleName, category, contextDescriptor) {
    343    return this.#data.some(item =>
    344      this.#matchItem(item, moduleName, category, contextDescriptor)
    345    );
    346  }
    347 
    348  /**
    349   * Update session data items of a given module, category and
    350   * contextDescriptor and propagate the information
    351   * via a command to existing MessageHandlers.
    352   *
    353   * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates
    354   *     Array of session data item updates.
    355   */
    356  async updateSessionData(sessionDataItemUpdates = []) {
    357    const updates = this.applySessionData(sessionDataItemUpdates);
    358 
    359    if (!updates.length) {
    360      // Avoid unnecessary broadcast if no items were updated.
    361      return;
    362    }
    363 
    364    // Create a Map with the structure moduleName -> category -> list of descriptors.
    365    const structuredUpdates = new Map();
    366    for (const { moduleName, category, contextDescriptor } of updates) {
    367      if (!structuredUpdates.has(moduleName)) {
    368        structuredUpdates.set(moduleName, new Map());
    369      }
    370      if (!structuredUpdates.get(moduleName).has(category)) {
    371        structuredUpdates.get(moduleName).set(category, new Set());
    372      }
    373      const descriptors = structuredUpdates.get(moduleName).get(category);
    374      // If there is at least one update for all contexts,
    375      // keep only this descriptor in the list of descriptors
    376      if (contextDescriptor.type === lazy.ContextDescriptorType.All) {
    377        structuredUpdates
    378          .get(moduleName)
    379          .set(category, new Set([contextDescriptor]));
    380      }
    381      // Add an individual descriptor if there is no descriptor for all contexts.
    382      else if (
    383        descriptors.size !== 1 ||
    384        Array.from(descriptors)[0]?.type !== lazy.ContextDescriptorType.All
    385      ) {
    386        descriptors.add(contextDescriptor);
    387      }
    388    }
    389 
    390    const rootDestination = {
    391      type: lazy.RootMessageHandler.type,
    392    };
    393    const sessionDataPromises = [];
    394 
    395    for (const [moduleName, categories] of structuredUpdates.entries()) {
    396      for (const [category, contextDescriptors] of categories.entries()) {
    397        // Find sessionData for the category and the moduleName.
    398        const relevantSessionData = this.#data.filter(
    399          item => item.category == category && item.moduleName === moduleName
    400        );
    401        for (const contextDescriptor of contextDescriptors.values()) {
    402          const windowGlobalDestination = {
    403            type: lazy.WindowGlobalMessageHandler.type,
    404            contextDescriptor,
    405          };
    406 
    407          for (const destination of [
    408            windowGlobalDestination,
    409            rootDestination,
    410          ]) {
    411            // Only apply session data if the module is present for the destination.
    412            if (
    413              this.#messageHandler.supportsCommand(
    414                moduleName,
    415                "_applySessionData",
    416                destination
    417              )
    418            ) {
    419              sessionDataPromises.push(
    420                this.#messageHandler
    421                  .handleCommand({
    422                    moduleName,
    423                    commandName: "_applySessionData",
    424                    params: {
    425                      sessionData: relevantSessionData,
    426                      category,
    427                      contextDescriptor,
    428                      initial: false,
    429                    },
    430                    destination,
    431                  })
    432                  ?.catch(reason =>
    433                    lazy.logger.error(
    434                      `_applySessionData for module: ${moduleName} failed, reason: ${reason}`
    435                    )
    436                  )
    437              );
    438            }
    439          }
    440        }
    441      }
    442    }
    443 
    444    await Promise.allSettled(sessionDataPromises);
    445  }
    446 
    447  #isSameItem(item1, item2) {
    448    const descriptor1 = item1.contextDescriptor;
    449    const descriptor2 = item2.contextDescriptor;
    450 
    451    return (
    452      item1.moduleName === item2.moduleName &&
    453      item1.category === item2.category &&
    454      this.#isSameContextDescriptor(descriptor1, descriptor2) &&
    455      this.#isSameValue(item1.category, item1.value, item2.value)
    456    );
    457  }
    458 
    459  #isSameContextDescriptor(contextDescriptor1, contextDescriptor2) {
    460    if (contextDescriptor1.type === lazy.ContextDescriptorType.All) {
    461      // Ignore the id for type "all" since we made the id optional for this type.
    462      return contextDescriptor1.type === contextDescriptor2.type;
    463    }
    464 
    465    return (
    466      contextDescriptor1.type === contextDescriptor2.type &&
    467      contextDescriptor1.id === contextDescriptor2.id
    468    );
    469  }
    470 
    471  #isSameValue(category, value1, value2) {
    472    if (category === SessionDataCategory.PreloadScript) {
    473      return value1.script === value2.script;
    474    }
    475 
    476    return value1 === value2;
    477  }
    478 
    479  #findIndex(item) {
    480    return this.#data.findIndex(_item => this.#isSameItem(item, _item));
    481  }
    482 
    483  #matchItem(item, moduleName, category, contextDescriptor) {
    484    return (
    485      item.moduleName === moduleName &&
    486      (!category || item.category === category) &&
    487      (!contextDescriptor ||
    488        this.#isSameContextDescriptor(
    489          item.contextDescriptor,
    490          contextDescriptor
    491        ))
    492    );
    493  }
    494 
    495  #persist() {
    496    // Update the sessionDataMap singleton.
    497    sessionDataMap.set(this.#messageHandler.sessionId, this.#data);
    498 
    499    // Update sharedData and flush to force consistency.
    500    Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
    501    Services.ppmm.sharedData.flush();
    502  }
    503 }