/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs", DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", Downloads: "resource://gre/modules/Downloads.sys.mjs", FileUtils: "resource://gre/modules/FileUtils.sys.mjs", ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", }); /** * Gets the filename automatically or by a file picker depending on "browser.download.useDownloadDir" * @param filenameTitle The title of the current page * @param browser The current browser * @returns Path of the chosen filename */ export async function getFilename(filenameTitle, browser) { if (filenameTitle === null) { filenameTitle = await lazy.ScreenshotsUtils.getActor(browser).sendQuery( "Screenshots:getDocumentTitle" ); } const date = new Date(); /* eslint-disable no-control-regex */ filenameTitle = filenameTitle .replace(/[\\/]/g, "_") .replace(/[\u200e\u200f\u202a-\u202e]/g, "") .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ") .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, ""); /* eslint-enable no-control-regex */ filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " "); const currentDateTime = new Date( date.getTime() - date.getTimezoneOffset() * 60 * 1000 ).toISOString(); const filenameDate = currentDateTime.substring(0, 10); const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-"); let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`; // Crop the filename size at less than 246 bytes, so as to leave // room for the extension and an ellipsis [...]. Note that JS // strings are UTF16 but the filename will be converted to UTF8 // when saving which could take up more space, and we want a // maximum of 255 bytes (not characters). Here, we iterate // and crop at shorter and shorter points until we fit into // 255 bytes. let suffix = ""; for (let cropSize = 246; cropSize >= 0; cropSize -= 32) { if (new Blob([clipFilename]).size > 246) { clipFilename = clipFilename.substring(0, cropSize); suffix = "[...]"; } else { break; } } clipFilename += suffix; let extension = ".png"; let filename = clipFilename + extension; let useDownloadDir = Services.prefs.getBoolPref( "browser.download.useDownloadDir" ); if (useDownloadDir) { const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory(); const downloadsDirExists = await IOUtils.exists(downloadsDir); if (downloadsDirExists) { // If filename is absolute, it will override the downloads directory and // still be applied as expected. filename = PathUtils.join(downloadsDir, filename); } } else { let fileInfo = new FileInfo(filename); let file; let fpParams = { fpTitleKey: "SaveImageTitle", fileInfo, contentType: "image/png", saveAsType: 0, file, }; let accepted = await promiseTargetFile(fpParams, browser.ownerGlobal); if (!accepted) { return null; } filename = fpParams.file.path; } return filename; } // The below functions are a modified copy from toolkit/content/contentAreaUtils.js /** * Structure for holding info about a URL and the target filename it should be * saved to. * @param aFileName The target filename */ class FileInfo { constructor(aFileName) { this.fileName = aFileName; this.fileBaseName = aFileName.replace(".png", ""); this.fileExt = "png"; } } const ContentAreaUtils = { get stringBundle() { delete this.stringBundle; return (this.stringBundle = Services.strings.createBundle( "chrome://global/locale/contentAreaCommands.properties" )); }, }; function makeFilePicker() { const fpContractID = "@mozilla.org/filepicker;1"; const fpIID = Ci.nsIFilePicker; return Cc[fpContractID].createInstance(fpIID); } function getMIMEService() { const mimeSvcContractID = "@mozilla.org/mime;1"; const mimeSvcIID = Ci.nsIMIMEService; const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID); return mimeSvc; } function getMIMEInfoForType(aMIMEType, aExtension) { if (aMIMEType || aExtension) { try { return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); } catch (e) {} } return null; } // This is only used after the user has entered a filename. function validateFileName(aFileName) { let processed = lazy.DownloadPaths.sanitize(aFileName, { compressWhitespaces: false, allowInvalidFilenames: true, }) || "_"; if (AppConstants.platform == "android") { // If a large part of the filename has been sanitized, then we // will use a default filename instead if (processed.replace(/_/g, "").length <= processed.length / 2) { // We purposefully do not use a localized default filename, // which we could have done using // ContentAreaUtils.stringBundle.GetStringFromName("UntitledSaveFileName") // since it may contain invalid characters. let original = processed; processed = "download"; // Preserve a suffix, if there is one if (original.includes(".")) { let suffix = original.split(".").pop(); if (suffix && !suffix.includes("_")) { processed += "." + suffix; } } } } return processed; } function appendFiltersForContentType( aFilePicker, aContentType, aFileExtension ) { let mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); if (mimeInfo) { let extString = ""; for (let extension of mimeInfo.getFileExtensions()) { if (extString) { extString += "; "; } // If adding more than one extension, // separate by semi-colon extString += "*." + extension; } if (extString) { aFilePicker.appendFilter(mimeInfo.description, extString); } } // Always append the all files (*) filter aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll); } /** * Given the Filepicker Parameters (aFpP), show the file picker dialog, * prompting the user to confirm (or change) the fileName. * @param aFpP * A structure (see definition in internalSave(...) method) * containing all the data used within this method. * @param win * The window used for opening the file picker * @return Promise * @resolve a boolean. When true, it indicates that the file picker dialog * is accepted. */ function promiseTargetFile(aFpP, win) { return (async function () { let downloadLastDir = new lazy.DownloadLastDir(win); // Default to the user's default downloads directory configured // through download prefs. let dirPath = await lazy.Downloads.getPreferredDownloadsDirectory(); let dirExists = await IOUtils.exists(dirPath); let dir = new lazy.FileUtils.File(dirPath); // We must prompt for the file name explicitly. // If we must prompt because we were asked to... let file = await downloadLastDir.getFileAsync(null); if (file && (await IOUtils.exists(file.path))) { dir = file; dirExists = true; } if (!dirExists) { // Default to desktop. dir = Services.dirsvc.get("Desk", Ci.nsIFile); } let fp = makeFilePicker(); let titleKey = aFpP.fpTitleKey; fp.init( win, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), Ci.nsIFilePicker.modeSave ); fp.displayDirectory = dir; fp.defaultExtension = aFpP.fileInfo.fileExt; fp.defaultString = aFpP.fileInfo.fileName; appendFiltersForContentType(fp, aFpP.contentType, aFpP.fileInfo.fileExt); let result = await new Promise(resolve => { fp.open(function (aResult) { resolve(aResult); }); }); if (result == Ci.nsIFilePicker.returnCancel || !fp.file) { return false; } // Do not store the last save directory as a pref inside the private browsing mode downloadLastDir.setFile(null, fp.file.parent); fp.file.leafName = validateFileName(fp.file.leafName); aFpP.saveAsType = fp.filterIndex; aFpP.file = fp.file; aFpP.fileURL = fp.fileURL; return true; })(); }