tor-browser

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

newSources.js (12908B)


      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 /**
      6 * Redux actions for the sources state
      7 *
      8 * @module actions/sources
      9 */
     10 import { insertSourceActors } from "../../actions/source-actors";
     11 import {
     12  makeSourceId,
     13  createGeneratedSource,
     14  createSourceMapOriginalSource,
     15  createSourceActor,
     16 } from "../../client/firefox/create";
     17 import { toggleBlackBox } from "./blackbox";
     18 import { syncPendingBreakpoint } from "../breakpoints/index";
     19 import { loadSourceText } from "./loadSourceText";
     20 import { prettyPrintAndSelectSource } from "./prettyPrint";
     21 import { toggleSourceMapIgnoreList } from "../ui";
     22 import { selectLocation, setBreakableLines } from "../sources/index";
     23 
     24 import { getRawSourceURL, isPrettyURL } from "../../utils/source";
     25 import { createLocation } from "../../utils/location";
     26 import {
     27  getBlackBoxRanges,
     28  getSource,
     29  getSourceFromId,
     30  hasSourceActor,
     31  getSourceByActorId,
     32  getPendingSelectedLocation,
     33  getPendingBreakpointsForSource,
     34  getSelectedLocation,
     35 } from "../../selectors/index";
     36 
     37 import { prefs } from "../../utils/prefs";
     38 import sourceQueue from "../../utils/source-queue";
     39 import { validateSourceActor, ContextError } from "../../utils/context";
     40 
     41 function loadSourceMapsForSourceActors(sourceActors) {
     42  return async function ({ dispatch }) {
     43    try {
     44      await Promise.all(
     45        sourceActors.map(sourceActor => dispatch(loadSourceMap(sourceActor)))
     46      );
     47    } catch (error) {
     48      // This may throw a context error if we navigated while processing the source maps
     49      if (!(error instanceof ContextError)) {
     50        throw error;
     51      }
     52    }
     53 
     54    // Once all the source maps, of all the bulk of new source actors are processed,
     55    // flush the SourceQueue. This help aggregate all the original sources in one action.
     56    await sourceQueue.flush();
     57  };
     58 }
     59 
     60 /**
     61 * @memberof actions/sources
     62 * @static
     63 */
     64 function loadSourceMap(sourceActor) {
     65  return async function ({ dispatch, getState, sourceMapLoader, panel }) {
     66    if (!prefs.clientSourceMapsEnabled || !sourceActor.sourceMapURL) {
     67      return;
     68    }
     69 
     70    let sources, ignoreListUrls, resolvedSourceMapURL, exception;
     71    try {
     72      // Ignore sourceMapURL on scripts that are part of HTML files, since
     73      // we currently treat sourcemaps as Source-wide, not SourceActor-specific.
     74      const source = getSourceByActorId(getState(), sourceActor.id);
     75      if (source) {
     76        ({ sources, ignoreListUrls, resolvedSourceMapURL, exception } =
     77          await sourceMapLoader.loadSourceMap({
     78            // Using source ID here is historical and eventually we'll want to
     79            // switch to all of this being per-source-actor.
     80            id: source.id,
     81            url: sourceActor.url || "",
     82            sourceMapBaseURL: sourceActor.sourceMapBaseURL || "",
     83            sourceMapURL: sourceActor.sourceMapURL || "",
     84            isWasm: sourceActor.introductionType === "wasm",
     85          }));
     86      }
     87    } catch (e) {
     88      exception = `Internal error: ${e.message}`;
     89    }
     90 
     91    if (resolvedSourceMapURL) {
     92      dispatch({
     93        type: "RESOLVED_SOURCEMAP_URL",
     94        sourceActor,
     95        resolvedSourceMapURL,
     96      });
     97    }
     98 
     99    if (ignoreListUrls?.length) {
    100      dispatch({
    101        type: "ADD_SOURCEMAP_IGNORE_LIST_SOURCES",
    102        ignoreListUrls,
    103      });
    104    }
    105 
    106    if (exception) {
    107      // Catch all errors and log them to the Web Console for users to see.
    108      const message = L10N.getFormatStr(
    109        "toolbox.sourceMapFailure",
    110        exception,
    111        sourceActor.url,
    112        sourceActor.sourceMapURL
    113      );
    114      panel.toolbox.commands.targetCommand.targetFront.logWarningInPage(
    115        message,
    116        "source map",
    117        resolvedSourceMapURL
    118      );
    119 
    120      dispatch({
    121        type: "SOURCE_MAP_ERROR",
    122        sourceActor,
    123        errorMessage: exception,
    124      });
    125 
    126      // If this source doesn't have a sourcemap or there are no original files
    127      // existing, enable it for pretty printing
    128      dispatch({
    129        type: "CLEAR_SOURCE_ACTOR_MAP_URL",
    130        sourceActor,
    131      });
    132      return;
    133    }
    134 
    135    // Before dispatching this action, ensure that the related sourceActor is still registered
    136    validateSourceActor(getState(), sourceActor);
    137 
    138    for (const originalSource of sources) {
    139      // The Source Map worker doesn't set the `sourceActor` attribute,
    140      // which is handy to know what is the related bundle.
    141      originalSource.sourceActor = sourceActor;
    142    }
    143 
    144    // Register all the new reported original sources in the queue to be flushed once all new bundles are processed.
    145    sourceQueue.queueOriginalSources(sources);
    146  };
    147 }
    148 
    149 // If a request has been made to show this source, go ahead and
    150 // select it.
    151 function checkSelectedSource(sourceId) {
    152  return async ({ dispatch, getState }) => {
    153    const state = getState();
    154    const pendingLocation = getPendingSelectedLocation(state);
    155 
    156    if (!pendingLocation || !pendingLocation.url) {
    157      return;
    158    }
    159 
    160    const source = getSource(state, sourceId);
    161 
    162    if (!source || !source.url) {
    163      return;
    164    }
    165 
    166    const pendingUrl = pendingLocation.url;
    167    const rawPendingUrl = getRawSourceURL(pendingUrl);
    168 
    169    if (rawPendingUrl === source.url) {
    170      if (isPrettyURL(pendingUrl)) {
    171        const prettySource = await dispatch(prettyPrintAndSelectSource(source));
    172        dispatch(checkPendingBreakpoints(prettySource, null));
    173        return;
    174      }
    175 
    176      await dispatch(
    177        selectLocation(
    178          createLocation({
    179            source,
    180            line:
    181              typeof pendingLocation.line === "number"
    182                ? pendingLocation.line
    183                : 0,
    184            column: pendingLocation.column,
    185          })
    186        )
    187      );
    188    }
    189  };
    190 }
    191 
    192 function checkPendingBreakpoints(source, sourceActor) {
    193  return async ({ dispatch, getState }) => {
    194    const pendingBreakpoints = getPendingBreakpointsForSource(
    195      getState(),
    196      source
    197    );
    198 
    199    if (pendingBreakpoints.length === 0) {
    200      return;
    201    }
    202 
    203    // load the source text if there is a pending breakpoint for it
    204    await dispatch(loadSourceText(source, sourceActor));
    205    await dispatch(setBreakableLines(createLocation({ source, sourceActor })));
    206 
    207    await Promise.all(
    208      pendingBreakpoints.map(pendingBp => {
    209        return dispatch(syncPendingBreakpoint(source, pendingBp));
    210      })
    211    );
    212  };
    213 }
    214 
    215 function restoreBlackBoxedSources(sources) {
    216  return async ({ dispatch, getState }) => {
    217    const currentRanges = getBlackBoxRanges(getState());
    218 
    219    if (!Object.keys(currentRanges).length) {
    220      return;
    221    }
    222 
    223    for (const source of sources) {
    224      const ranges = currentRanges[source.url];
    225      if (ranges) {
    226        // If the ranges is an empty then the whole source was blackboxed.
    227        await dispatch(toggleBlackBox(source, true, ranges));
    228      }
    229    }
    230 
    231    if (prefs.sourceMapIgnoreListEnabled) {
    232      await dispatch(toggleSourceMapIgnoreList(true));
    233    }
    234  };
    235 }
    236 
    237 export function newOriginalSources(originalSourcesInfo) {
    238  return async ({ dispatch, getState }) => {
    239    const state = getState();
    240    const seen = new Set();
    241 
    242    const actors = [];
    243    const actorsSources = {};
    244 
    245    for (const { id, url, sourceActor } of originalSourcesInfo) {
    246      if (seen.has(id) || getSource(state, id)) {
    247        continue;
    248      }
    249      seen.add(id);
    250 
    251      if (!actorsSources[sourceActor.actor]) {
    252        actors.push(sourceActor);
    253        actorsSources[sourceActor.actor] = [];
    254      }
    255 
    256      actorsSources[sourceActor.actor].push(
    257        createSourceMapOriginalSource(id, url, sourceActor.sourceObject)
    258      );
    259    }
    260 
    261    // Add the original sources per the generated source actors that
    262    // they are primarily from.
    263    actors.forEach(sourceActor => {
    264      dispatch({
    265        type: "ADD_ORIGINAL_SOURCES",
    266        originalSources: actorsSources[sourceActor.actor],
    267        generatedSourceActor: sourceActor,
    268      });
    269    });
    270 
    271    // Accumulate the sources back into one list
    272    const actorsSourcesValues = Object.values(actorsSources);
    273    let sources = [];
    274    if (actorsSourcesValues.length) {
    275      sources = actorsSourcesValues.reduce((acc, sourceList) =>
    276        acc.concat(sourceList)
    277      );
    278    }
    279 
    280    await dispatch(checkNewSources(sources));
    281 
    282    for (const source of sources) {
    283      dispatch(checkPendingBreakpoints(source, null));
    284    }
    285 
    286    return sources;
    287  };
    288 }
    289 
    290 // Wrapper around newGeneratedSources, only used by tests
    291 export function newGeneratedSource(sourceInfo) {
    292  return async ({ dispatch }) => {
    293    const sources = await dispatch(newGeneratedSources([sourceInfo]));
    294    return sources[0];
    295  };
    296 }
    297 
    298 export function newGeneratedSources(sourceResources) {
    299  return async ({ dispatch, getState }) => {
    300    if (!sourceResources.length) {
    301      return [];
    302    }
    303 
    304    const resultIds = [];
    305    const newSourcesObj = {};
    306    const newSourceActors = [];
    307 
    308    for (const sourceResource of sourceResources) {
    309      // By the time we process the sources, the related target
    310      // might already have been destroyed. It means that the sources
    311      // are also about to be destroyed, so ignore them.
    312      // (This is covered by browser_toolbox_backward_forward_navigation.js)
    313      if (sourceResource.targetFront.isDestroyed()) {
    314        continue;
    315      }
    316      const id = makeSourceId(sourceResource);
    317 
    318      if (!getSource(getState(), id) && !newSourcesObj[id]) {
    319        newSourcesObj[id] = createGeneratedSource(sourceResource);
    320      }
    321 
    322      const actorId = sourceResource.actor;
    323 
    324      // We are sometimes notified about a new source multiple times if we
    325      // request a new source list and also get a source event from the server.
    326      if (!hasSourceActor(getState(), actorId)) {
    327        newSourceActors.push(
    328          createSourceActor(
    329            sourceResource,
    330            getSource(getState(), id) || newSourcesObj[id]
    331          )
    332        );
    333      }
    334 
    335      resultIds.push(id);
    336    }
    337 
    338    const newSources = Object.values(newSourcesObj);
    339 
    340    dispatch({ type: "ADD_SOURCES", sources: newSources });
    341    dispatch(insertSourceActors(newSourceActors));
    342 
    343    await dispatch(checkNewSources(newSources));
    344 
    345    (async () => {
    346      await dispatch(loadSourceMapsForSourceActors(newSourceActors));
    347 
    348      // We have to force fetching the breakable lines for any incoming source actor
    349      // related to HTML page as we may have the HTML page selected,
    350      // and already fetched its breakable lines and won't try to update
    351      // the breakable lines for any late coming inline <script> tag.
    352      const selectedLocation = getSelectedLocation(getState());
    353      for (const sourceActor of newSourceActors) {
    354        if (
    355          selectedLocation?.source == sourceActor.sourceObject &&
    356          sourceActor.sourceObject.isHTML
    357        ) {
    358          await dispatch(
    359            setBreakableLines(
    360              createLocation({ source: sourceActor.sourceObject, sourceActor })
    361            )
    362          );
    363        }
    364      }
    365 
    366      // We would like to sync breakpoints after we are done
    367      // loading source maps as sometimes generated and original
    368      // files share the same paths.
    369      for (const sourceActor of newSourceActors) {
    370        dispatch(
    371          checkPendingBreakpoints(sourceActor.sourceObject, sourceActor)
    372        );
    373      }
    374    })();
    375 
    376    return resultIds.map(id => getSourceFromId(getState(), id));
    377  };
    378 }
    379 
    380 /**
    381 * Common operations done against generated and original sources,
    382 * just after having registered them in the reducers:
    383 *  - automatically selecting the source if it matches the last known selected source
    384 *    (i.e. the pending selected location).
    385 *  - automatically notify the server about new sources that used to be blackboxed.
    386 *    The blackboxing is per Source Actor and so we need to notify them individually
    387 *    if the source used to be ignored.
    388 */
    389 function checkNewSources(sources) {
    390  return async ({ dispatch }) => {
    391    // Waiting for `checkSelectedSource` completion is important for pretty printed sources.
    392    // `checkPendingBreakpoints`, which is called after this method, is expected to be called
    393    // only after the source is mapped and breakpoints positions are updated with the mapped locations.
    394    // For source mapped sources, `loadSourceMapsForSourceActors` is called before calling
    395    // `checkPendingBreakpoints` and will do all that. For pretty printed source, we rely on
    396    // `checkSelectedSource` to do that. Selecting a minimized source which used to be pretty printed
    397    // will automatically force pretty printing it and computing the mapped breakpoint positions.
    398    await Promise.all(
    399      sources.map(
    400        async source => await dispatch(checkSelectedSource(source.id))
    401      )
    402    );
    403 
    404    await dispatch(restoreBlackBoxedSources(sources));
    405 
    406    return sources;
    407  };
    408 }