323 lines
11 KiB
JavaScript
323 lines
11 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 = {};
|
|
// Windows has a total path length of 259 characters so we have to calculate
|
|
// the max filename length by
|
|
// MAX_PATH_LENGTH_WINDOWS - downloadDir length - null terminator character
|
|
// in the function getMaxFilenameLength below.
|
|
export const MAX_PATH_LENGTH_WINDOWS = 259;
|
|
// Windows allows 255 character filenames in the filepicker
|
|
// macOS has a max filename length of 255 characters
|
|
// Linux has a max filename length of 255 bytes
|
|
export const MAX_FILENAME_LENGTH = 255;
|
|
|
|
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",
|
|
});
|
|
|
|
/**
|
|
* macOS and Linux have a max filename of 255.
|
|
* Windows allows 259 as the total path length so we have to calculate the max
|
|
* filename length if the download directory exists. Otherwise, Windows allows
|
|
* 255 character filenames in the filepicker.
|
|
*
|
|
* @param {string} downloadDir The current download directory or null
|
|
* @returns {number} The max filename length
|
|
*/
|
|
export function getMaxFilenameLength(downloadDir = null) {
|
|
if (!downloadDir || AppConstants.platform !== "win") {
|
|
return MAX_FILENAME_LENGTH;
|
|
}
|
|
|
|
return MAX_PATH_LENGTH_WINDOWS - downloadDir.length - 1;
|
|
}
|
|
|
|
/**
|
|
* Linux has a max length of bytes while macOS and Windows has a max length of
|
|
* characters so we have to check them differently.
|
|
*
|
|
* @param {string} filename The current clipped filename
|
|
* @param {string} maxFilenameLength The max length of the filename
|
|
* @returns {boolean} True if the filename is too long, otherwise false
|
|
*/
|
|
function checkFilenameLength(filename, maxFilenameLength) {
|
|
if (AppConstants.platform === "linux") {
|
|
return new Blob([filename]).size > maxFilenameLength;
|
|
}
|
|
|
|
return filename.length > maxFilenameLength;
|
|
}
|
|
|
|
/**
|
|
* 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 = getMaxFilenameLength(knownDownloadsDir);
|
|
/* 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 -= 1) {
|
|
if (checkFilenameLength(clipFilename, 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;
|
|
})();
|
|
}
|