tor-browser

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

screenshot.js (13182B)


      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 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     13  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     14  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     15 });
     16 
     17 const STRINGS_URI = "devtools/shared/locales/screenshot.properties";
     18 const L10N = new LocalizationHelper(STRINGS_URI);
     19 
     20 /**
     21 * Take a screenshot of a browser element matching the passed target and save it to a file
     22 * or the clipboard.
     23 *
     24 * @param {TargetFront} targetFront: The targetFront of the frame we want to take a screenshot of.
     25 * @param {Window} window: The DevTools Client window.
     26 * @param {object} args
     27 * @param {boolean} args.fullpage: Should the screenshot be the height of the whole page
     28 * @param {string} args.filename: Expected filename for the screenshot
     29 * @param {boolean} args.clipboard: Whether or not the screenshot should be saved to the clipboard.
     30 * @param {number} args.dpr: Scale of the screenshot. Defaults to the window `devicePixelRatio`.
     31 *                           ⚠️ Note that the scale might be decreased if the resulting
     32 *                           image would be too big to draw safely. Warning will be emitted
     33 *                           to the console if that's the case.
     34 * @param {number} args.delay: Number of seconds to wait before taking the screenshot
     35 * @param {boolean} args.help: Set to true to receive a message with the screenshot command
     36 *                             documentation.
     37 * @param {boolean} args.disableFlash: Set to true to disable the flash animation when the
     38 *                  screenshot is taken.
     39 * @param {boolean} args.ignoreDprForFileScale: Set to true to if the resulting screenshot
     40 *                  file size shouldn't be impacted by the dpr. Note that the dpr will still
     41 *                  be taken into account when taking the screenshot, only the size of the
     42 *                  file will be different.
     43 * @returns {Array<Object{text, level}>} An array of object representing the different
     44 *          messages emitted throught the process, that should be displayed to the user.
     45 */
     46 async function captureAndSaveScreenshot(targetFront, window, args = {}) {
     47  if (args.help) {
     48    // Wrap message in an array so that the return value is consistant.
     49    return [{ text: getFormattedHelpData() }];
     50  }
     51 
     52  const captureResponse = await captureScreenshot(targetFront, args);
     53 
     54  if (captureResponse.error) {
     55    return captureResponse.messages || [];
     56  }
     57 
     58  const saveMessages = await saveScreenshot(window, args, captureResponse);
     59  return (captureResponse.messages || []).concat(saveMessages);
     60 }
     61 
     62 /**
     63 * Take a screenshot of a browser element matching the passed target
     64 *
     65 * @param {TargetFront} targetFront: The targetFront of the frame we want to take a screenshot of.
     66 * @param {object} args: See args param in captureAndSaveScreenshot
     67 */
     68 async function captureScreenshot(targetFront, args) {
     69  // @backward-compat { version 87 } The screenshot-content actor was introduced in 87,
     70  // so we can always use it once 87 reaches release.
     71  const supportsContentScreenshot = targetFront.hasActor("screenshotContent");
     72  if (!supportsContentScreenshot) {
     73    const screenshotFront = await targetFront.getFront("screenshot");
     74    return screenshotFront.capture(args);
     75  }
     76 
     77  if (args.delay > 0) {
     78    await new Promise(res => setTimeout(res, args.delay * 1000));
     79  }
     80 
     81  const screenshotContentFront =
     82    await targetFront.getFront("screenshot-content");
     83 
     84  // Call the content-process on the server to retrieve informations that will be needed
     85  // by the parent process.
     86  const { rect, windowDpr, windowZoom, messages, error } =
     87    await screenshotContentFront.prepareCapture(args);
     88 
     89  if (error) {
     90    return { error, messages };
     91  }
     92 
     93  if (rect) {
     94    args.rect = rect;
     95  }
     96 
     97  args.dpr ||= windowDpr;
     98 
     99  args.snapshotScale = args.dpr * windowZoom;
    100  if (args.ignoreDprForFileScale) {
    101    args.fileScale = windowZoom;
    102  }
    103 
    104  args.browsingContextID = targetFront.browsingContextID;
    105 
    106  // We can now call the parent process which will take the screenshot via
    107  // the drawSnapshot API
    108  const rootFront = targetFront.client.mainRoot;
    109  const parentProcessScreenshotFront = await rootFront.getFront("screenshot");
    110  const captureResponse = await parentProcessScreenshotFront.capture(args);
    111 
    112  return {
    113    ...captureResponse,
    114    messages: (messages || []).concat(captureResponse.messages || []),
    115  };
    116 }
    117 
    118 const screenshotDescription = L10N.getStr("screenshotDesc");
    119 const screenshotGroupOptions = L10N.getStr("screenshotGroupOptions");
    120 const screenshotCommandParams = [
    121  {
    122    name: "clipboard",
    123    type: "boolean",
    124    description: L10N.getStr("screenshotClipboardDesc"),
    125    manual: L10N.getStr("screenshotClipboardManual"),
    126  },
    127  {
    128    name: "delay",
    129    type: "number",
    130    description: L10N.getStr("screenshotDelayDesc"),
    131    manual: L10N.getStr("screenshotDelayManual"),
    132  },
    133  {
    134    name: "dpr",
    135    type: "number",
    136    description: L10N.getStr("screenshotDPRDesc"),
    137    manual: L10N.getStr("screenshotDPRManual"),
    138  },
    139  {
    140    name: "fullpage",
    141    type: "boolean",
    142    description: L10N.getStr("screenshotFullPageDesc"),
    143    manual: L10N.getStr("screenshotFullPageManual"),
    144  },
    145  {
    146    name: "selector",
    147    type: "string",
    148    description: L10N.getStr("inspectNodeDesc"),
    149    manual: L10N.getStr("inspectNodeManual"),
    150  },
    151  {
    152    name: "file",
    153    type: "boolean",
    154    description: L10N.getStr("screenshotFileDesc"),
    155    manual: L10N.getStr("screenshotFileManual"),
    156  },
    157  {
    158    name: "filename",
    159    type: "string",
    160    description: L10N.getStr("screenshotFilenameDesc"),
    161    manual: L10N.getStr("screenshotFilenameManual"),
    162  },
    163 ];
    164 
    165 /**
    166 * Creates a string from an object for use when screenshot is passed the `--help` argument
    167 *
    168 * @param object param
    169 *        The param object to be formatted.
    170 * @return string
    171 *         The formatted information from the param object as a string
    172 */
    173 function formatHelpField(param) {
    174  const padding = " ".repeat(5);
    175  return Object.entries(param)
    176    .map(([key, value]) => {
    177      if (key === "name") {
    178        const name = `${padding}--${value}`;
    179        return name;
    180      }
    181      return `${padding.repeat(2)}${key}: ${value}`;
    182    })
    183    .join("\n");
    184 }
    185 
    186 /**
    187 * Creates a string response from the screenshot options for use when
    188 * screenshot is passed the `--help` argument
    189 *
    190 * @return string
    191 *         The formatted information from the param object as a string
    192 */
    193 function getFormattedHelpData() {
    194  const formattedParams = screenshotCommandParams
    195    .map(formatHelpField)
    196    .join("\n\n");
    197 
    198  return `${screenshotDescription}\n${screenshotGroupOptions}\n\n${formattedParams}`;
    199 }
    200 
    201 /**
    202 * Main entry point in this file; Takes the original arguments that `:screenshot` was
    203 * called with and the image value from the server, and uses the client window to add
    204 * and audio effect.
    205 *
    206 * @param object window
    207 *        The DevTools Client window.
    208 *
    209 * @param object args
    210 *        The original args with which the screenshot
    211 *        was called.
    212 * @param object value
    213 *        an object with a image value and file name
    214 *
    215 * @return string[]
    216 *         Response messages from processing the screenshot
    217 */
    218 function saveScreenshot(window, args = {}, value) {
    219  // @backward-compat { version 87 } This is still needed by the console when connecting
    220  // to an older server. Once 87 is in release, we can remove this whole block since we
    221  // already handle args.help in captureScreenshotAndSave.
    222  if (args.help) {
    223    // Wrap message in an array so that the return value is consistant.
    224    return [{ text: getFormattedHelpData() }];
    225  }
    226 
    227  // Guard against missing image data.
    228  if (!value.data) {
    229    return [];
    230  }
    231 
    232  simulateCameraShutter(window);
    233  return save(window, args, value);
    234 }
    235 
    236 /**
    237 * This function is called to simulate camera effects
    238 *
    239 * @param object document
    240 *        The DevTools Client document.
    241 */
    242 function simulateCameraShutter(window) {
    243  if (Services.prefs.getBoolPref("devtools.screenshot.audio.enabled")) {
    244    const audioCamera = new window.Audio(
    245      "resource://devtools/client/themes/audio/shutter.wav"
    246    );
    247    audioCamera.play();
    248  }
    249 }
    250 
    251 /**
    252 * Save the captured screenshot to one of several destinations.
    253 *
    254 * @param object window
    255 *        The DevTools Client window.
    256 *
    257 * @param object args
    258 *        The original args with which the screenshot was called.
    259 *
    260 * @param object image
    261 *        The image object that was sent from the server.
    262 *
    263 *
    264 * @return string[]
    265 *         Response messages from processing the screenshot.
    266 */
    267 async function save(window, args, image) {
    268  const fileNeeded = args.filename || !args.clipboard || args.file;
    269  const results = [];
    270 
    271  if (args.clipboard) {
    272    const result = saveToClipboard(image.data);
    273    results.push(result);
    274  }
    275 
    276  if (fileNeeded) {
    277    const result = await saveToFile(window, image);
    278    results.push(result);
    279  }
    280  return results;
    281 }
    282 
    283 /**
    284 * Save the image data to the clipboard. This returns a promise, so it can
    285 * be treated exactly like file processing.
    286 *
    287 * @param string base64URI
    288 *        The image data encoded in a base64 URI that was sent from the server.
    289 *
    290 * @return string
    291 *         Response message from processing the screenshot.
    292 */
    293 function saveToClipboard(base64URI) {
    294  try {
    295    const imageTools = Cc["@mozilla.org/image/tools;1"].getService(
    296      Ci.imgITools
    297    );
    298 
    299    const base64Data = base64URI.replace("data:image/png;base64,", "");
    300 
    301    const image = atob(base64Data);
    302    const img = imageTools.decodeImageFromBuffer(
    303      image,
    304      image.length,
    305      "image/png"
    306    );
    307 
    308    const transferable = Cc[
    309      "@mozilla.org/widget/transferable;1"
    310    ].createInstance(Ci.nsITransferable);
    311    transferable.init(null);
    312    transferable.addDataFlavor("image/png");
    313    transferable.setTransferData("image/png", img);
    314 
    315    Services.clipboard.setData(
    316      transferable,
    317      null,
    318      Services.clipboard.kGlobalClipboard
    319    );
    320    return { text: L10N.getStr("screenshotCopied") };
    321  } catch (ex) {
    322    console.error(ex);
    323    return { level: "error", text: L10N.getStr("screenshotErrorCopying") };
    324  }
    325 }
    326 
    327 let _outputDirectory = null;
    328 
    329 /**
    330 * Returns the default directory for DevTools screenshots.
    331 * For consistency with the Firefox Screenshots feature, this will default to
    332 * the preferred downloads directory.
    333 *
    334 * @return {Promise<string>} Resolves the path as a string
    335 */
    336 async function getOutputDirectory() {
    337  if (_outputDirectory) {
    338    return _outputDirectory;
    339  }
    340 
    341  _outputDirectory = await lazy.Downloads.getPreferredScreenshotsDirectory();
    342  return _outputDirectory;
    343 }
    344 
    345 /**
    346 * Save the screenshot data to disk, returning a promise which is resolved on
    347 * completion.
    348 *
    349 * @param object window
    350 *        The DevTools Client window.
    351 *
    352 * @param object image
    353 *        The image object that was sent from the server.
    354 *
    355 * @return string
    356 *         Response message from processing the screenshot.
    357 */
    358 async function saveToFile(window, image) {
    359  let filename = image.filename;
    360 
    361  // Guard against missing image data.
    362  if (!image.data) {
    363    return "";
    364  }
    365 
    366  // Check there is a .png extension to filename
    367  if (!filename.match(/.png$/i)) {
    368    filename += ".png";
    369  }
    370 
    371  const dir = await getOutputDirectory();
    372  const dirExists = await IOUtils.exists(dir);
    373  if (dirExists) {
    374    // If filename is absolute, it will override the downloads directory and
    375    // still be applied as expected.
    376    filename = PathUtils.isAbsolute(filename)
    377      ? filename
    378      : PathUtils.joinRelative(dir, filename);
    379  }
    380 
    381  const targetFile = new lazy.FileUtils.File(filename);
    382 
    383  // Create download and track its progress.
    384  try {
    385    const download = await lazy.Downloads.createDownload({
    386      source: {
    387        url: image.data,
    388        // Here we want to know if the window in which the screenshot is taken is private.
    389        // We have a ChromeWindow when this is called from Browser Console (:screenshot) and
    390        // RDM (screenshot button).
    391        isPrivate: window.isChromeWindow
    392          ? lazy.PrivateBrowsingUtils.isWindowPrivate(window)
    393          : lazy.PrivateBrowsingUtils.isBrowserPrivate(
    394              window.browsingContext.embedderElement
    395            ),
    396      },
    397      target: targetFile,
    398    });
    399    const list = await lazy.Downloads.getList(lazy.Downloads.ALL);
    400    // add the download to the download list in the Downloads list in the Browser UI
    401    list.add(download);
    402    // Await successful completion of the save via the download manager
    403    await download.start();
    404    return { text: L10N.getFormatStr("screenshotSavedToFile", filename) };
    405  } catch (ex) {
    406    console.error(ex);
    407    return {
    408      level: "error",
    409      text: L10N.getFormatStr("screenshotErrorSavingToFile", filename),
    410    };
    411  }
    412 }
    413 
    414 module.exports = {
    415  captureAndSaveScreenshot,
    416  captureScreenshot,
    417  saveScreenshot,
    418 };