forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			290 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			290 lines
		
	
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 = {};
 | |
| // The maximum length of a pathanme - calculated as MAX_PATH minus the null terminator character
 | |
| export const MAX_PATHNAME = AppConstants.platform == "win" ? 259 : 1023;
 | |
| export const MAX_LEAFNAME = MAX_PATHNAME - 32;
 | |
| export const FALLBACK_MAX_LEAFNAME = 64;
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   Downloads: "resource://gre/modules/Downloads.sys.mjs",
 | |
|   DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs",
 | |
|   DownloadPaths: "resource://gre/modules/DownloadPaths.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();
 | |
|   const knownDownloadsDir = await getDownloadDirectory();
 | |
|   // if we know the download directory, we can subtract that plus the separator from MAX_PATHNAME to get a length limit
 | |
|   // otherwise we just use a conservative length
 | |
|   const maxFilenameLength = Math.min(
 | |
|     knownDownloadsDir
 | |
|       ? MAX_PATHNAME - new Blob([knownDownloadsDir]).size - 1
 | |
|       : FALLBACK_MAX_LEAFNAME,
 | |
|     MAX_LEAFNAME
 | |
|   );
 | |
|   /* 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}`;
 | |
| 
 | |
|   // allow space for a potential ellipsis and the extension
 | |
|   let maxNameStemLength = maxFilenameLength - "[...].png".length;
 | |
| 
 | |
|   // Crop the filename size 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 maxFilenameLength bytes (not characters). Here, we iterate
 | |
|   // and crop at shorter and shorter points until we fit into
 | |
|   // our max number of bytes.
 | |
|   let suffix = "";
 | |
|   for (let cropSize = maxNameStemLength; cropSize >= 0; cropSize -= 32) {
 | |
|     if (new Blob([clipFilename]).size > maxNameStemLength) {
 | |
|       clipFilename = clipFilename.substring(0, cropSize);
 | |
|       suffix = "[...]";
 | |
|     } else {
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
|   clipFilename += suffix;
 | |
| 
 | |
|   let extension = ".png";
 | |
|   let filename = clipFilename + extension;
 | |
| 
 | |
|   if (knownDownloadsDir) {
 | |
|     // If filename is absolute, it will override the downloads directory and
 | |
|     // still be applied as expected.
 | |
|     filename = PathUtils.join(knownDownloadsDir, 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 { filename: null, accepted };
 | |
|     }
 | |
|     filename = fpParams.file.path;
 | |
|   }
 | |
|   return { filename, accepted: true };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Gets the path to the download directory if "browser.download.useDownloadDir" is true
 | |
|  * @returns Path to download directory or null if not available
 | |
|  */
 | |
| export async function getDownloadDirectory() {
 | |
|   let useDownloadDir = Services.prefs.getBoolPref(
 | |
|     "browser.download.useDownloadDir"
 | |
|   );
 | |
|   if (useDownloadDir) {
 | |
|     const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory();
 | |
|     if (await IOUtils.exists(downloadsDir)) {
 | |
|       return downloadsDir;
 | |
|     }
 | |
|   }
 | |
|   return null;
 | |
| }
 | |
| 
 | |
| // 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.browsingContext,
 | |
|       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);
 | |
| 
 | |
|     aFpP.saveAsType = fp.filterIndex;
 | |
|     aFpP.file = fp.file;
 | |
|     aFpP.file.leafName = validateFileName(aFpP.file.leafName);
 | |
| 
 | |
|     return true;
 | |
|   })();
 | |
| }
 | 
