tor-browser

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

HeadlessShell.sys.mjs (7663B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
      6 import { HiddenFrame } from "resource://gre/modules/HiddenFrame.sys.mjs";
      7 
      8 // Refrences to the progress listeners to keep them from being gc'ed
      9 // before they are called.
     10 const progressListeners = new Set();
     11 
     12 export class ScreenshotParent extends JSWindowActorParent {
     13  getDimensions(params) {
     14    return this.sendQuery("GetDimensions", params);
     15  }
     16 }
     17 
     18 ChromeUtils.registerWindowActor("Screenshot", {
     19  parent: {
     20    esModuleURI: "moz-src:///browser/components/shell/HeadlessShell.sys.mjs",
     21  },
     22  child: {
     23    esModuleURI: "moz-src:///browser/components/shell/ScreenshotChild.sys.mjs",
     24  },
     25 });
     26 
     27 function loadContentWindow(browser, url) {
     28  let uri = URL.parse(url)?.URI;
     29  if (!uri) {
     30    let err = new Error(`Invalid URL passed to loadContentWindow(): ${url}`);
     31    console.error(err);
     32    return Promise.reject(err);
     33  }
     34 
     35  const principal = Services.scriptSecurityManager.getSystemPrincipal();
     36  return new Promise(resolve => {
     37    let oa = E10SUtils.predictOriginAttributes({
     38      browser,
     39    });
     40    let loadURIOptions = {
     41      triggeringPrincipal: principal,
     42      remoteType: E10SUtils.getRemoteTypeForURI(
     43        url,
     44        true,
     45        false,
     46        E10SUtils.DEFAULT_REMOTE_TYPE,
     47        null,
     48        oa
     49      ),
     50    };
     51    browser.loadURI(uri, loadURIOptions);
     52    let { webProgress } = browser;
     53 
     54    let progressListener = {
     55      onLocationChange(progress, request, location, flags) {
     56        // Ignore inner-frame events
     57        if (!progress.isTopLevel) {
     58          return;
     59        }
     60        // Ignore events that don't change the document
     61        if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
     62          return;
     63        }
     64        // Ignore the initial about:blank, unless about:blank is requested
     65        if (location.spec == "about:blank" && uri.spec != "about:blank") {
     66          return;
     67        }
     68 
     69        progressListeners.delete(progressListener);
     70        webProgress.removeProgressListener(progressListener);
     71        resolve();
     72      },
     73      QueryInterface: ChromeUtils.generateQI([
     74        "nsIWebProgressListener",
     75        "nsISupportsWeakReference",
     76      ]),
     77    };
     78    progressListeners.add(progressListener);
     79    webProgress.addProgressListener(
     80      progressListener,
     81      Ci.nsIWebProgress.NOTIFY_LOCATION
     82    );
     83  });
     84 }
     85 
     86 async function takeScreenshot(
     87  fullWidth,
     88  fullHeight,
     89  contentWidth,
     90  contentHeight,
     91  path,
     92  url
     93 ) {
     94  let frame;
     95  try {
     96    frame = new HiddenFrame();
     97    let windowlessBrowser = await frame.get();
     98 
     99    let doc = windowlessBrowser.document;
    100    let browser = doc.createXULElement("browser");
    101    browser.setAttribute("remote", "true");
    102    browser.setAttribute("type", "content");
    103    browser.style.width = `${contentWidth}px`;
    104    browser.style.minWidth = `${contentWidth}px`;
    105    browser.style.height = `${contentHeight}px`;
    106    browser.style.minHeight = `${contentHeight}px`;
    107    browser.setAttribute("maychangeremoteness", "true");
    108    doc.documentElement.appendChild(browser);
    109 
    110    await loadContentWindow(browser, url);
    111 
    112    let actor =
    113      browser.browsingContext.currentWindowGlobal.getActor("Screenshot");
    114    let dimensions = await actor.getDimensions();
    115 
    116    let canvas = doc.createElementNS(
    117      "http://www.w3.org/1999/xhtml",
    118      "html:canvas"
    119    );
    120    let context = canvas.getContext("2d");
    121    let width = dimensions.innerWidth;
    122    let height = dimensions.innerHeight;
    123    if (fullWidth) {
    124      width += dimensions.scrollMaxX - dimensions.scrollMinX;
    125    }
    126    if (fullHeight) {
    127      height += dimensions.scrollMaxY - dimensions.scrollMinY;
    128    }
    129    canvas.width = width;
    130    canvas.height = height;
    131    let rect = new DOMRect(0, 0, width, height);
    132 
    133    let snapshot =
    134      await browser.browsingContext.currentWindowGlobal.drawSnapshot(
    135        rect,
    136        1,
    137        "rgb(255, 255, 255)"
    138      );
    139    context.drawImage(snapshot, 0, 0);
    140 
    141    snapshot.close();
    142 
    143    let blob = await new Promise(resolve => canvas.toBlob(resolve));
    144 
    145    let reader = await new Promise(resolve => {
    146      let fr = new FileReader();
    147      fr.onloadend = () => resolve(fr);
    148      fr.readAsArrayBuffer(blob);
    149    });
    150 
    151    await IOUtils.write(path, new Uint8Array(reader.result));
    152    dump("Screenshot saved to: " + path + "\n");
    153  } catch (e) {
    154    dump("Failure taking screenshot: " + e + "\n");
    155  } finally {
    156    if (frame) {
    157      frame.destroy();
    158    }
    159  }
    160 }
    161 
    162 export let HeadlessShell = {
    163  async handleCmdLineArgs(cmdLine, URLlist) {
    164    try {
    165      // Don't quit even though we don't create a window
    166      Services.startup.enterLastWindowClosingSurvivalArea();
    167 
    168      // Default options
    169      let fullWidth = true;
    170      let fullHeight = true;
    171      // Most common screen resolution of Firefox users
    172      let contentWidth = 1366;
    173      let contentHeight = 768;
    174 
    175      // Parse `window-size`
    176      try {
    177        var dimensionsStr = cmdLine.handleFlagWithParam("window-size", true);
    178      } catch (e) {
    179        dump("expected format: --window-size width[,height]\n");
    180        return;
    181      }
    182      if (dimensionsStr) {
    183        let success;
    184        let dimensions = dimensionsStr.split(",", 2);
    185        if (dimensions.length == 1) {
    186          success = dimensions[0] > 0;
    187          if (success) {
    188            fullWidth = false;
    189            fullHeight = true;
    190            contentWidth = dimensions[0];
    191          }
    192        } else {
    193          success = dimensions[0] > 0 && dimensions[1] > 0;
    194          if (success) {
    195            fullWidth = false;
    196            fullHeight = false;
    197            contentWidth = dimensions[0];
    198            contentHeight = dimensions[1];
    199          }
    200        }
    201 
    202        if (!success) {
    203          dump("expected format: --window-size width[,height]\n");
    204          return;
    205        }
    206      }
    207 
    208      let urlOrFileToSave = null;
    209      try {
    210        urlOrFileToSave = cmdLine.handleFlagWithParam("screenshot", true);
    211      } catch (e) {
    212        // We know that the flag exists so we only get here if there was no parameter.
    213        cmdLine.handleFlag("screenshot", true); // Remove `screenshot`
    214      }
    215 
    216      // Assume that the remaining arguments that do not start
    217      // with a hyphen are URLs
    218      for (let i = 0; i < cmdLine.length; ++i) {
    219        const argument = cmdLine.getArgument(i);
    220        if (argument.startsWith("-")) {
    221          dump(`Warning: unrecognized command line flag ${argument}\n`);
    222          // To emulate the pre-nsICommandLine behavior, we ignore
    223          // the argument after an unrecognized flag.
    224          ++i;
    225        } else {
    226          URLlist.push(argument);
    227        }
    228      }
    229 
    230      let path = null;
    231      if (urlOrFileToSave && !URLlist.length) {
    232        // URL was specified next to "-screenshot"
    233        // Example: -screenshot https://www.example.com -attach-console
    234        URLlist.push(urlOrFileToSave);
    235      } else {
    236        path = urlOrFileToSave;
    237      }
    238 
    239      if (!path) {
    240        path = PathUtils.join(cmdLine.workingDirectory.path, "screenshot.png");
    241      }
    242 
    243      if (URLlist.length == 1) {
    244        await takeScreenshot(
    245          fullWidth,
    246          fullHeight,
    247          contentWidth,
    248          contentHeight,
    249          path,
    250          URLlist[0]
    251        );
    252      } else {
    253        dump("expected exactly one URL when using `screenshot`\n");
    254      }
    255    } finally {
    256      Services.startup.exitLastWindowClosingSurvivalArea();
    257      Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
    258    }
    259  },
    260 };