tor-browser

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

fileHelpers.mjs (11069B)


      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 { AppConstants } = ChromeUtils.importESModule(
      6  "resource://gre/modules/AppConstants.sys.mjs"
      7 );
      8 const { XPCOMUtils } = ChromeUtils.importESModule(
      9  "resource://gre/modules/XPCOMUtils.sys.mjs"
     10 );
     11 
     12 const lazy = {};
     13 // Windows has a total path length of 259 characters so we have to calculate
     14 // the max filename length by
     15 // MAX_PATH_LENGTH_WINDOWS - downloadDir length - null terminator character
     16 // in the function getMaxFilenameLength below.
     17 export const MAX_PATH_LENGTH_WINDOWS = 259;
     18 // Windows allows 255 character filenames in the filepicker
     19 // macOS has a max filename length of 255 characters
     20 // Linux has a max filename length of 255 bytes
     21 export const MAX_FILENAME_LENGTH = 255;
     22 
     23 ChromeUtils.defineESModuleGetters(lazy, {
     24  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     25  DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs",
     26  DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
     27  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     28  ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
     29 });
     30 
     31 XPCOMUtils.defineLazyPreferenceGetter(
     32  lazy,
     33  "useDownloadDir",
     34  "browser.download.useDownloadDir",
     35  true
     36 );
     37 
     38 /**
     39 * macOS and Linux have a max filename of 255.
     40 * Windows allows 259 as the total path length so we have to calculate the max
     41 * filename length if the download directory exists. Otherwise, Windows allows
     42 * 255 character filenames in the filepicker.
     43 *
     44 * @param {string} downloadDir The current download directory or null
     45 * @returns {number} The max filename length
     46 */
     47 export function getMaxFilenameLength(downloadDir = null) {
     48  if (!downloadDir || AppConstants.platform !== "win") {
     49    return MAX_FILENAME_LENGTH;
     50  }
     51 
     52  return MAX_PATH_LENGTH_WINDOWS - downloadDir.length - 1;
     53 }
     54 
     55 /**
     56 * Linux has a max length of bytes while macOS and Windows has a max length of
     57 * characters so we have to check them differently.
     58 *
     59 * @param {string} filename The current clipped filename
     60 * @param {string} maxFilenameLength The max length of the filename
     61 * @returns {boolean} True if the filename is too long, otherwise false
     62 */
     63 function checkFilenameLength(filename, maxFilenameLength) {
     64  if (AppConstants.platform === "linux") {
     65    return new Blob([filename]).size > maxFilenameLength;
     66  }
     67 
     68  return filename.length > maxFilenameLength;
     69 }
     70 
     71 /**
     72 * Gets the filename automatically or by a file picker depending on "browser.download.useDownloadDir"
     73 *
     74 * @param filenameTitle The title of the current page
     75 * @param browser The current browser
     76 * @returns Path of the chosen filename
     77 */
     78 export async function getFilename(filenameTitle, browser) {
     79  if (filenameTitle === null) {
     80    filenameTitle = await lazy.ScreenshotsUtils.getActor(browser).sendQuery(
     81      "Screenshots:getDocumentTitle"
     82    );
     83  }
     84  const date = new Date();
     85  const knownDownloadsDir = await getDownloadDirectory();
     86  // if we know the download directory, we can subtract that plus the separator from MAX_PATHNAME to get a length limit
     87  // otherwise we just use a conservative length
     88  const maxFilenameLength = getMaxFilenameLength(knownDownloadsDir);
     89  /* eslint-disable no-control-regex */
     90  filenameTitle = filenameTitle
     91    .replace(/[\\/]/g, "_")
     92    .replace(/[\u200e\u200f\u202a-\u202e]/g, "")
     93    .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ")
     94    .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
     95  /* eslint-enable no-control-regex */
     96  filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
     97  const currentDateTime = new Date(
     98    date.getTime() - date.getTimezoneOffset() * 60 * 1000
     99  ).toISOString();
    100  const filenameDate = currentDateTime.substring(0, 10);
    101  const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-");
    102  let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`;
    103 
    104  // allow space for a potential ellipsis and the extension
    105  let maxNameStemLength = maxFilenameLength - "[...].png".length;
    106 
    107  // Crop the filename size so as to leave
    108  // room for the extension and an ellipsis [...]. Note that JS
    109  // strings are UTF16 but the filename will be converted to UTF8
    110  // when saving which could take up more space, and we want a
    111  // maximum of maxFilenameLength bytes (not characters). Here, we iterate
    112  // and crop at shorter and shorter points until we fit into
    113  // our max number of bytes.
    114  let suffix = "";
    115  for (let cropSize = maxNameStemLength; cropSize >= 0; cropSize -= 1) {
    116    if (checkFilenameLength(clipFilename, maxNameStemLength)) {
    117      clipFilename = clipFilename.substring(0, cropSize);
    118      suffix = "[...]";
    119    } else {
    120      break;
    121    }
    122  }
    123  clipFilename += suffix;
    124 
    125  let extension = ".png";
    126  let filename = clipFilename + extension;
    127 
    128  if (knownDownloadsDir) {
    129    // If filename is absolute, it will override the downloads directory and
    130    // still be applied as expected.
    131    filename = PathUtils.join(knownDownloadsDir, filename);
    132  } else {
    133    let fileInfo = new FileInfo(filename);
    134    let file;
    135    let fpParams = {
    136      fpTitleKey: "SaveImageTitle",
    137      fileInfo,
    138      contentType: "image/png",
    139      saveAsType: 0,
    140      file,
    141    };
    142    let accepted = await promiseTargetFile(fpParams, browser.ownerGlobal);
    143    if (!accepted) {
    144      return { filename: null, accepted };
    145    }
    146    filename = fpParams.file.path;
    147  }
    148  return { filename, accepted: true };
    149 }
    150 
    151 /**
    152 * Gets the path to the preferred screenshots directory if "browser.download.useDownloadDir" is true
    153 *
    154 * @returns Path to preferred screenshots directory or null if not available
    155 */
    156 export async function getDownloadDirectory() {
    157  if (lazy.useDownloadDir) {
    158    const downloadsDir =
    159      await lazy.Downloads.getPreferredScreenshotsDirectory();
    160    if (await IOUtils.exists(downloadsDir)) {
    161      return downloadsDir;
    162    }
    163  }
    164  return null;
    165 }
    166 
    167 // The below functions are a modified copy from toolkit/content/contentAreaUtils.js
    168 /**
    169 * Structure for holding info about a URL and the target filename it should be
    170 * saved to.
    171 *
    172 * @param aFileName The target filename
    173 */
    174 class FileInfo {
    175  constructor(aFileName) {
    176    this.fileName = aFileName;
    177    this.fileBaseName = aFileName.replace(".png", "");
    178    this.fileExt = "png";
    179  }
    180 }
    181 
    182 const ContentAreaUtils = {
    183  get stringBundle() {
    184    delete this.stringBundle;
    185    return (this.stringBundle = Services.strings.createBundle(
    186      "chrome://global/locale/contentAreaCommands.properties"
    187    ));
    188  },
    189 };
    190 
    191 function makeFilePicker() {
    192  const fpContractID = "@mozilla.org/filepicker;1";
    193  const fpIID = Ci.nsIFilePicker;
    194  return Cc[fpContractID].createInstance(fpIID);
    195 }
    196 
    197 function getMIMEService() {
    198  const mimeSvcContractID = "@mozilla.org/mime;1";
    199  const mimeSvcIID = Ci.nsIMIMEService;
    200  const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID);
    201  return mimeSvc;
    202 }
    203 
    204 function getMIMEInfoForType(aMIMEType, aExtension) {
    205  if (aMIMEType || aExtension) {
    206    try {
    207      return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension);
    208    } catch (e) {}
    209  }
    210  return null;
    211 }
    212 
    213 // This is only used after the user has entered a filename.
    214 function validateFileName(aFileName) {
    215  let processed =
    216    lazy.DownloadPaths.sanitize(aFileName, {
    217      compressWhitespaces: false,
    218      allowInvalidFilenames: true,
    219    }) || "_";
    220  if (AppConstants.platform == "android") {
    221    // If a large part of the filename has been sanitized, then we
    222    // will use a default filename instead
    223    if (processed.replace(/_/g, "").length <= processed.length / 2) {
    224      // We purposefully do not use a localized default filename,
    225      // which we could have done using
    226      // ContentAreaUtils.stringBundle.GetStringFromName("UntitledSaveFileName")
    227      // since it may contain invalid characters.
    228      let original = processed;
    229      processed = "download";
    230 
    231      // Preserve a suffix, if there is one
    232      if (original.includes(".")) {
    233        let suffix = original.split(".").pop();
    234        if (suffix && !suffix.includes("_")) {
    235          processed += "." + suffix;
    236        }
    237      }
    238    }
    239  }
    240  return processed;
    241 }
    242 
    243 function appendFiltersForContentType(
    244  aFilePicker,
    245  aContentType,
    246  aFileExtension
    247 ) {
    248  let mimeInfo = getMIMEInfoForType(aContentType, aFileExtension);
    249  if (mimeInfo) {
    250    let extString = "";
    251    for (let extension of mimeInfo.getFileExtensions()) {
    252      if (extString) {
    253        extString += "; ";
    254      } // If adding more than one extension,
    255      // separate by semi-colon
    256      extString += "*." + extension;
    257    }
    258 
    259    if (extString) {
    260      aFilePicker.appendFilter(mimeInfo.description, extString);
    261    }
    262  }
    263 
    264  // Always append the all files (*) filter
    265  aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll);
    266 }
    267 
    268 /**
    269 * Given the Filepicker Parameters (aFpP), show the file picker dialog,
    270 * prompting the user to confirm (or change) the fileName.
    271 *
    272 * @param aFpP
    273 *        A structure (see definition in internalSave(...) method)
    274 *        containing all the data used within this method.
    275 * @param win
    276 *        The window used for opening the file picker
    277 * @returns {Promise<boolean>}
    278 *   Resolves when the dialog is closed. When resolved to true, it indicates that
    279 *   the file picker dialog is accepted.
    280 */
    281 function promiseTargetFile(aFpP, win) {
    282  return (async function () {
    283    let downloadLastDir = new lazy.DownloadLastDir(win);
    284 
    285    // Default to the user's default downloads directory configured
    286    // through download prefs.
    287    let dirPath = await lazy.Downloads.getPreferredDownloadsDirectory();
    288    let dirExists = await IOUtils.exists(dirPath);
    289    let dir = new lazy.FileUtils.File(dirPath);
    290 
    291    // We must prompt for the file name explicitly.
    292    // If we must prompt because we were asked to...
    293    let file = await downloadLastDir.getFileAsync(null);
    294    if (file && (await IOUtils.exists(file.path))) {
    295      dir = file;
    296      dirExists = true;
    297    }
    298 
    299    if (!dirExists) {
    300      // Default to desktop.
    301      dir = Services.dirsvc.get("Desk", Ci.nsIFile);
    302    }
    303 
    304    let fp = makeFilePicker();
    305    let titleKey = aFpP.fpTitleKey;
    306    fp.init(
    307      win.browsingContext,
    308      ContentAreaUtils.stringBundle.GetStringFromName(titleKey),
    309      Ci.nsIFilePicker.modeSave
    310    );
    311 
    312    fp.displayDirectory = dir;
    313    fp.defaultExtension = aFpP.fileInfo.fileExt;
    314    fp.defaultString = aFpP.fileInfo.fileName;
    315    appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt);
    316 
    317    let result = await new Promise(resolve => {
    318      fp.open(function (aResult) {
    319        resolve(aResult);
    320      });
    321    });
    322    if (result == Ci.nsIFilePicker.returnCancel || !fp.file) {
    323      return false;
    324    }
    325 
    326    // Do not store the last save directory as a pref inside the private browsing mode
    327    downloadLastDir.setFile(null, fp.file.parent);
    328 
    329    aFpP.saveAsType = fp.filterIndex;
    330    aFpP.file = fp.file;
    331    aFpP.file.leafName = validateFileName(aFpP.file.leafName);
    332 
    333    return true;
    334  })();
    335 }