Bug 1402279 - Part 2 - Unify the usage of the DownloadPaths module. r=mak,aswan

MozReview-Commit-ID: HEhwkyxtYTP

--HG--
extra : rebase_source : d17ef98796d5a4fcac899c6319045eec02b0633e
This commit is contained in:
Paolo Amadini 2017-09-26 16:03:24 +01:00
parent 8418fc1ba5
commit 9c4cbe557f
5 changed files with 127 additions and 43 deletions

View File

@ -429,7 +429,7 @@ this.downloads = class extends ExtensionAPI {
return Promise.reject({message: "filename must not contain back-references (..)"});
}
if (AppConstants.platform === "win" && /[|"*?:<>]/.test(filename)) {
if (path.components.some(component => component != DownloadPaths.sanitize(component))) {
return Promise.reject({message: "filename must not contain illegal characters"});
}
}
@ -472,19 +472,15 @@ this.downloads = class extends ExtensionAPI {
}
async function createTarget(downloadsDir) {
let target;
if (filename) {
target = OS.Path.join(downloadsDir, filename);
} else {
if (!filename) {
let uri = Services.io.newURI(options.url);
let remote;
if (uri instanceof Ci.nsIURL) {
remote = uri.fileName;
filename = DownloadPaths.sanitize(uri.fileName);
}
target = OS.Path.join(downloadsDir, remote || "download");
}
let target = OS.Path.join(downloadsDir, filename || "download");
// Create any needed subdirectories if required by filename.
const dir = OS.Path.dirname(target);
await OS.File.makeDir(dir, {from: downloadsDir});

View File

@ -14,7 +14,70 @@ this.EXPORTED_SYMBOLS = [
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
/**
* Platform-dependent regular expression used by the "sanitize" method.
*/
XPCOMUtils.defineLazyGetter(this, "gConvertToSpaceRegExp", () => {
/* eslint-disable no-control-regex */
switch (AppConstants.platform) {
// On mobile devices, the file system may be very limited in what it
// considers valid characters. To avoid errors, sanitize conservatively.
case "android":
return /[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g;
case "win":
return /[\x00-\x1f\x7f-\x9f:*?|]+/g;
case "macosx":
return /[\x00-\x1f\x7f-\x9f:]+/g;
default:
return /[\x00-\x1f\x7f-\x9f]+/g;
}
/* eslint-enable no-control-regex */
});
this.DownloadPaths = {
/**
* Sanitizes an arbitrary string for use as the local file name of a download.
* The input is often a document title or a manually edited name. The output
* can be an empty string if the input does not include any valid character.
*
* The length of the resulting string is not limited, because restrictions
* apply to the full path name after the target folder has been added.
*
* Splitting the base name and extension to add a counter or to identify the
* file type should only be done after the sanitization process, because it
* can alter the final part of the string or remove leading dots.
*
* Runs of slashes and backslashes are replaced with an underscore.
*
* On Windows, the angular brackets `<` and `>` are replaced with parentheses,
* and double quotes are replaced with single quotes.
*
* Runs of control characters are replaced with a space. On Mac, colons are
* also included in this group. On Windows, stars, question marks, and pipes
* are additionally included. On Android, semicolons, commas, plus signs,
* equal signs, and brackets are additionally included.
*
* Leading and trailing dots and whitespace are removed on all platforms. This
* avoids the accidental creation of hidden files on Unix and invalid or
* inaccessible file names on Windows. These characters are not removed when
* located at the end of the base name or at the beginning of the extension.
*/
sanitize(leafName) {
if (AppConstants.platform == "win") {
leafName = leafName.replace(/</g, "(")
.replace(/>/g, ")")
.replace(/"/g, "'");
}
return leafName.replace(/[\\/]+/g, "_")
.replace(gConvertToSpaceRegExp, " ")
.replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
},
/**
* Creates a uniquely-named file starting from the name of the provided file.
* If a file with the provided name already exists, the function attempts to

View File

@ -5,6 +5,8 @@
* Tests for the "DownloadPaths.jsm" JavaScript module.
*/
Cu.import("resource://gre/modules/AppConstants.jsm");
/**
* Provides a temporary save directory.
*
@ -20,6 +22,10 @@ function createTemporarySaveDirectory() {
return saveDir;
}
function testSanitize(leafName, expectedLeafName) {
do_check_eq(DownloadPaths.sanitize(leafName), expectedLeafName);
}
function testSplitBaseNameAndExtension(aLeafName, [aBase, aExt]) {
var [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
do_check_eq(base, aBase);
@ -41,6 +47,53 @@ function testCreateNiceUniqueFile(aTempFile, aExpectedLeafName) {
do_check_eq(createdFile.leafName, aExpectedLeafName);
}
add_task(async function test_sanitize() {
// Platform-dependent conversion of special characters to spaces.
const kSpecialChars = "A:*?|\"\"<<>>;,+=[]B][=+,;>><<\"\"|?*:C";
if (AppConstants.platform == "android") {
testSanitize(kSpecialChars, "A B C");
testSanitize(" :: Website :: ", "Website");
testSanitize("* Website!", "Website!");
testSanitize("Website | Page!", "Website Page!");
testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
} else if (AppConstants.platform == "win") {
testSanitize(kSpecialChars, "A ''(());,+=[]B][=+,;))(('' C");
testSanitize(" :: Website :: ", "Website");
testSanitize("* Website!", "Website!");
testSanitize("Website | Page!", "Website Page!");
testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
} else if (AppConstants.platform == "macosx") {
testSanitize(kSpecialChars, "A *?|\"\"<<>>;,+=[]B][=+,;>><<\"\"|?* C");
testSanitize(" :: Website :: ", "Website");
testSanitize("* Website!", "* Website!");
testSanitize("Website | Page!", "Website | Page!");
testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_");
} else {
testSanitize(kSpecialChars, kSpecialChars);
testSanitize(" :: Website :: ", ":: Website ::");
testSanitize("* Website!", "* Website!");
testSanitize("Website | Page!", "Website | Page!");
testSanitize("Directory Listing: /a/b/", "Directory Listing: _a_b_");
}
// Conversion of consecutive runs of slashes and backslashes to underscores.
testSanitize("\\ \\\\Website\\/Page// /", "_ _Website_Page_ _");
// Removal of leading and trailing whitespace and dots after conversion.
testSanitize(" Website ", "Website");
testSanitize(". . Website . Page . .", "Website . Page");
testSanitize(" File . txt ", "File . txt");
testSanitize("\f\n\r\t\v\x00\x1f\x7f\x80\x9f\xa0 . txt", "txt");
testSanitize("\u1680\u180e\u2000\u2008\u200a . txt", "txt");
testSanitize("\u2028\u2029\u202f\u205f\u3000\ufeff . txt", "txt");
// Strings with whitespace and dots only.
testSanitize(".", "");
testSanitize("..", "");
testSanitize(" ", "");
testSanitize(" . ", "");
});
add_task(async function test_splitBaseNameAndExtension() {
// Usual file names.
testSplitBaseNameAndExtension("base", ["base", ""]);

View File

@ -7,6 +7,7 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
Downloads: "resource://gre/modules/Downloads.jsm",
DownloadPaths: "resource://gre/modules/DownloadPaths.jsm",
DownloadLastDir: "resource://gre/modules/DownloadLastDir.jsm",
FileUtils: "resource://gre/modules/FileUtils.jsm",
OS: "resource://gre/modules/osfile.jsm",
@ -1059,29 +1060,8 @@ function getDefaultFileName(aDefaultFileName, aURI, aDocument,
}
function validateFileName(aFileName) {
var re = /[\/]+/g;
if (navigator.appVersion.indexOf("Windows") != -1) {
re = /[\\\/\|]+/g;
aFileName = aFileName.replace(/[\"]+/g, "'");
aFileName = aFileName.replace(/[\*\:\?]+/g, " ");
aFileName = aFileName.replace(/[\<]+/g, "(");
aFileName = aFileName.replace(/[\>]+/g, ")");
} else if (navigator.appVersion.indexOf("Macintosh") != -1)
re = /[\:\/]+/g;
else if (navigator.appVersion.indexOf("Android") != -1) {
// On mobile devices, the filesystem may be very limited in what
// it considers valid characters. To avoid errors, we sanitize
// conservatively.
const dangerousChars = "*?<>|\":/\\[];,+=";
var processed = "";
for (var i = 0; i < aFileName.length; i++)
processed += aFileName.charCodeAt(i) >= 32 &&
!(dangerousChars.indexOf(aFileName[i]) >= 0) ? aFileName[i]
: "_";
// Last character should not be a space
processed = processed.trim();
let processed = DownloadPaths.sanitize(aFileName) || "_";
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) {
@ -1099,10 +1079,8 @@ function validateFileName(aFileName) {
processed += "." + suffix;
}
}
return processed;
}
return aFileName.replace(re, "_");
return processed;
}
function getNormalizedLeafName(aFile, aDefaultExtension) {

View File

@ -370,14 +370,8 @@ nsUnknownContentTypeDialog.prototype = {
getFinalLeafName: function (aLeafName, aFileExt)
{
// Remove any leading periods, since we don't want to save hidden files
// automatically.
aLeafName = aLeafName.replace(/^\.+/, "");
if (aLeafName == "")
aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
return aLeafName;
return DownloadPaths.sanitize(aLeafName) ||
"unnamed" + (aFileExt ? "." + aFileExt : "");
},
/**